Skip to content

Commit 78ac5d7

Browse files
qwwdfsadmeta-codesync[bot]
authored andcommitted
Reuse Parser.parse results (Kotlin#622)
Summary: Cuts down formatting time on coroutines from 4.7 to ~2.9-3 seconds. All explanations are in commit messages (note: they should be reviewed separately, the first one addresses a non-functional but necessary change) Partially addresses Kotlin#619 Fixes Kotlin#552 Pull Request resolved: Kotlin#622 Reviewed By: cortinico Differential Revision: D108656440 Pulled By: hick209 fbshipit-source-id: b57200ad68139636b1a12d9680dbd5dd0be56195
1 parent a7249a2 commit 78ac5d7

8 files changed

Lines changed: 111 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
1515
### Changed
1616

1717
* Reduced overall number of allocations to improve formatting performance (~6-7%) (https://github.com/facebook/ktfmt/pull/620)
18+
* Reuse results of `Parser.parse` (https://github.com/facebook/ktfmt/pull/622)
1819

1920
## [0.63]
2021

core/api/ktfmt.api

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,6 @@ public final class com/facebook/ktfmt/format/KotlinToken : com/google/googlejava
296296

297297
public final class com/facebook/ktfmt/format/MultilineStringFormatter {
298298
public fun <init> (I)V
299-
public final fun format (Ljava/lang/String;)Ljava/lang/String;
300299
public final fun getContinuationIndentSize ()I
301300
}
302301

@@ -325,8 +324,6 @@ public final class com/facebook/ktfmt/format/PsiUtilsKt {
325324

326325
public final class com/facebook/ktfmt/format/RedundantElementManager {
327326
public static final field INSTANCE Lcom/facebook/ktfmt/format/RedundantElementManager;
328-
public final fun addRedundantElements (Ljava/lang/String;Lcom/facebook/ktfmt/format/FormattingOptions;)Ljava/lang/String;
329-
public final fun dropRedundantElements (Ljava/lang/String;Lcom/facebook/ktfmt/format/FormattingOptions;)Ljava/lang/String;
330327
}
331328

332329
public final class com/facebook/ktfmt/format/Tokenizer : org/jetbrains/kotlin/psi/KtTreeVisitorVoid {

core/src/main/java/com/facebook/ktfmt/format/Formatter.kt

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiComment
3636
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
3737
import org.jetbrains.kotlin.com.intellij.psi.PsiElementVisitor
3838
import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
39+
import org.jetbrains.kotlin.psi.KtFile
3940
import org.jetbrains.kotlin.psi.KtImportDirective
4041
import org.jetbrains.kotlin.psi.psiUtil.endOffset
4142
import org.jetbrains.kotlin.psi.psiUtil.startOffset
@@ -98,21 +99,21 @@ object Formatter {
9899
}
99100
checkEscapeSequences(kotlinCode)
100101

101-
return kotlinCode
102-
.let { convertLineSeparators(it) }
103-
.let { sortedAndDistinctImports(it) }
104-
.let { dropRedundantElements(it, options) }
105-
.let { addRedundantElements(it, options) }
106-
.let { prettyPrint(it, options, lineSeparator = "\n") }
107-
.let { addRedundantElements(it, options) }
108-
.let { MultilineStringFormatter(options.continuationIndent).format(it) }
102+
return FormatterContext(convertLineSeparators(kotlinCode))
103+
.transform { sortedAndDistinctImports(it) }
104+
.transform { dropRedundantElements(it, options) }
105+
.transform { addRedundantElements(it, options) }
106+
.transform { prettyPrint(it, options, lineSeparator = "\n") }
107+
.transform { addRedundantElements(it, options) }
108+
.transform { MultilineStringFormatter(options.continuationIndent).format(it) }
109+
.code
109110
.let { convertLineSeparators(it, checkNotNull(Newlines.guessLineSeparator(kotlinCode))) }
110111
.let { if (shebang.isEmpty()) it else shebang + "\n" + it }
111112
}
112113

113114
/** prettyPrint reflows 'code' using google-java-format's engine. */
114-
private fun prettyPrint(code: String, options: FormattingOptions, lineSeparator: String): String {
115-
val file = Parser.parse(code)
115+
private fun prettyPrint(file: KtFile, options: FormattingOptions, lineSeparator: String): String {
116+
val code = file.text
116117
val kotlinInput = KotlinInput(code, file)
117118
val javaOutput =
118119
JavaOutput(lineSeparator, kotlinInput, KDocCommentsHelper(lineSeparator, options.maxWidth))
@@ -157,8 +158,8 @@ object Formatter {
157158
}
158159
}
159160

160-
private fun sortedAndDistinctImports(code: String): String {
161-
val file = Parser.parse(code)
161+
private fun sortedAndDistinctImports(file: KtFile): String {
162+
val code = file.text
162163

163164
val importList = file.importList ?: return code
164165
if (importList.imports.isEmpty()) {
@@ -190,10 +191,20 @@ object Formatter {
190191
val sortedImports = importList.imports.sortedBy(::canonicalText).distinctBy(::canonicalText)
191192
val importsWithComments = commentList + sortedImports
192193

194+
val body = importsWithComments.joinToString(separator = "\n") { imprt -> imprt.text }
195+
/*
196+
* Kludge: idempotent formatting.
197+
* This step optimizes the following goal -- producing **identical** code for already formatted
198+
* code, as it's important for PSI-reuse.
199+
* There is exactly one case where this step should add trailing newline -- when an inline
200+
* comment follows the last import statement. We check for that (note it gives false positives for `/* // */`
201+
* which is acceptable -- later prettyPrint step will fix that) and avoid extra-append when it is redundant.
202+
*/
203+
val needsTerminator = body.lastIndexOf('\n').let { it >= 0 && body.indexOf("//", it + 1) >= 0 }
193204
return code.replaceRange(
194205
importList.startOffset,
195206
importList.endOffset,
196-
importsWithComments.joinToString(separator = "\n") { imprt -> imprt.text } + "\n",
207+
if (needsTerminator) body + "\n" else body,
197208
)
198209
}
199210
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.facebook.ktfmt.format
18+
19+
import org.jetbrains.kotlin.psi.KtFile
20+
21+
internal class FormatterContext(@JvmField val code: String) {
22+
23+
private val ktFile: KtFile by lazy { Parser.parse(code) }
24+
25+
inline fun transform(block: (KtFile) -> String): FormatterContext {
26+
val newCode = block(ktFile)
27+
return if (newCode == code) this else FormatterContext(newCode)
28+
}
29+
}

core/src/main/java/com/facebook/ktfmt/format/MultilineStringFormatter.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.facebook.ktfmt.format
1818

1919
import com.google.common.annotations.VisibleForTesting
2020
import org.jetbrains.kotlin.com.intellij.psi.PsiComment
21+
import org.jetbrains.kotlin.psi.KtFile
2122
import org.jetbrains.kotlin.psi.KtQualifiedExpression
2223
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
2324
import org.jetbrains.kotlin.psi.KtTreeVisitorVoid
@@ -31,10 +32,11 @@ private const val TQ = "\"\"\""
3132
* imports.
3233
*/
3334
class MultilineStringFormatter(val continuationIndentSize: Int) {
34-
fun format(code: String): String {
35+
internal fun format(file: KtFile): String {
36+
val code = file.text
3537
val result = StringBuilder(code)
3638
val multilineStringList =
37-
getMultilineTrimmedStringList(code)
39+
getMultilineTrimmedStringList(file)
3840
.sortedByDescending(MultilineTrimmedString::openStringOffset)
3941
for (multilineString in multilineStringList) {
4042
if (multilineString.stringLineCount < 2) {
@@ -104,8 +106,8 @@ class MultilineStringFormatter(val continuationIndentSize: Int) {
104106
}
105107

106108
@VisibleForTesting
107-
internal fun getMultilineTrimmedStringList(code: String): List<MultilineTrimmedString> {
108-
val file = Parser.parse(code)
109+
internal fun getMultilineTrimmedStringList(file: KtFile): List<MultilineTrimmedString> {
110+
val code = file.text
109111
val strings = mutableListOf<MultilineTrimmedString>()
110112
file.accept(
111113
object : KtTreeVisitorVoid() {

core/src/main/java/com/facebook/ktfmt/format/RedundantElementManager.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiElement
2020
import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
2121
import org.jetbrains.kotlin.kdoc.psi.impl.KDocImpl
2222
import org.jetbrains.kotlin.psi.KtElement
23+
import org.jetbrains.kotlin.psi.KtFile
2324
import org.jetbrains.kotlin.psi.KtImportList
2425
import org.jetbrains.kotlin.psi.KtPackageDirective
2526
import org.jetbrains.kotlin.psi.KtReferenceExpression
@@ -33,8 +34,8 @@ import org.jetbrains.kotlin.psi.psiUtil.startOffset
3334
*/
3435
object RedundantElementManager {
3536
/** Remove extra semicolons and unused imports, if enabled in the [options] */
36-
fun dropRedundantElements(code: String, options: FormattingOptions): String {
37-
val file = Parser.parse(code)
37+
internal fun dropRedundantElements(file: KtFile, options: FormattingOptions): String {
38+
val code = file.text
3839
val redundantImportDetector = RedundantImportDetector(enabled = options.removeUnusedImports)
3940
val redundantSemicolonDetector = RedundantSemicolonDetector()
4041
val trailingCommaDetector = TrailingCommas.Detector()
@@ -92,12 +93,12 @@ object RedundantElementManager {
9293
return result.toString()
9394
}
9495

95-
fun addRedundantElements(code: String, options: FormattingOptions): String {
96+
internal fun addRedundantElements(file: KtFile, options: FormattingOptions): String {
9697
if (!options.manageTrailingCommas) {
97-
return code
98+
return file.text
9899
}
99100

100-
val file = Parser.parse(code)
101+
val code = file.text
101102
val trailingCommaSuggestor = TrailingCommas.Suggestor()
102103

103104
file.accept(

core/src/test/java/com/facebook/ktfmt/format/FormatterTest.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,48 @@ class FormatterTest {
12191219
assertThatFormatting(code).isEqualTo(expected)
12201220
}
12211221

1222+
@Test
1223+
fun `imports with trailing block comment and expression`() {
1224+
val code =
1225+
"""
1226+
|import com.example.zab /* heya */
1227+
|import com.example.foo ; val x = Sample(foo, zab)
1228+
|"""
1229+
.trimMargin()
1230+
1231+
val expected =
1232+
"""
1233+
|import com.example.foo
1234+
|import com.example.zab /* heya */
1235+
|
1236+
|val x = Sample(foo, zab)
1237+
|"""
1238+
.trimMargin()
1239+
1240+
assertThatFormatting(code).isEqualTo(expected)
1241+
}
1242+
1243+
@Test
1244+
fun `imports with trailing block comment and expression with false positive double slash`() {
1245+
val code =
1246+
"""
1247+
|import com.example.zab /* // */
1248+
|import com.example.foo ; val x = Sample(foo, zab)
1249+
|"""
1250+
.trimMargin()
1251+
1252+
val expected =
1253+
"""
1254+
|import com.example.foo
1255+
|import com.example.zab /* // */
1256+
|
1257+
|val x = Sample(foo, zab)
1258+
|"""
1259+
.trimMargin()
1260+
1261+
assertThatFormatting(code).isEqualTo(expected)
1262+
}
1263+
12221264
@Test
12231265
fun `backticks are ignored in import sort order`() =
12241266
assertFormatted(

core/src/test/java/com/facebook/ktfmt/format/MultilineStringFormatterTest.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,9 @@ class MultilineStringFormatterTest {
303303
@Language("kts") code: String,
304304
continuationIndent: Int = 4,
305305
): MultilineTrimmedString {
306-
val strings = MultilineStringFormatter(continuationIndent).getMultilineTrimmedStringList(code)
306+
val strings =
307+
MultilineStringFormatter(continuationIndent)
308+
.getMultilineTrimmedStringList(Parser.parse(code))
307309
assertThat(strings.size).isEqualTo(1)
308310
return strings.first()
309311
}

0 commit comments

Comments
 (0)