Skip to content

Commit 9f21c3b

Browse files
Refactor PsiFileFactory to KtlintKotlinCompiler
This reduces the number of conversions between "psi" and ASTNode. Also, the code is simplified
1 parent f1d86cc commit 9f21c3b

File tree

9 files changed

+91
-159
lines changed

9 files changed

+91
-159
lines changed

ktlint-rule-engine-core/api/ktlint-rule-engine-core.api

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,12 @@ public final class com/pinterest/ktlint/rule/engine/core/api/IndentConfig$Indent
400400
public static fun values ()[Lcom/pinterest/ktlint/rule/engine/core/api/IndentConfig$IndentStyle;
401401
}
402402

403+
public final class com/pinterest/ktlint/rule/engine/core/api/KtlintKotlinCompiler {
404+
public static final field INSTANCE Lcom/pinterest/ktlint/rule/engine/core/api/KtlintKotlinCompiler;
405+
public final fun createASTNodeFromText (Ljava/lang/String;)Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;
406+
public final fun createPsiFileFromText (Ljava/lang/String;Ljava/lang/String;)Lorg/jetbrains/kotlin/com/intellij/psi/PsiFile;
407+
}
408+
403409
public class com/pinterest/ktlint/rule/engine/core/api/Rule {
404410
public fun <init> (Lcom/pinterest/ktlint/rule/engine/core/api/RuleId;Lcom/pinterest/ktlint/rule/engine/core/api/Rule$About;Ljava/util/Set;Ljava/util/Set;)V
405411
public synthetic fun <init> (Lcom/pinterest/ktlint/rule/engine/core/api/RuleId;Lcom/pinterest/ktlint/rule/engine/core/api/Rule$About;Ljava/util/Set;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V

ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/ASTNodeExtension.kt

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,13 @@ import com.pinterest.ktlint.rule.engine.core.api.ElementType.VARARG_KEYWORD
88
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VAR_KEYWORD
99
import com.pinterest.ktlint.rule.engine.core.api.ElementType.WHITE_SPACE
1010
import org.jetbrains.kotlin.KtNodeType
11-
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
12-
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
1311
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
14-
import org.jetbrains.kotlin.com.intellij.mock.MockProject
15-
import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer
1612
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
17-
import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory
1813
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement
1914
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement
2015
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl
2116
import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType
2217
import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet
23-
import org.jetbrains.kotlin.config.CompilerConfiguration
24-
import org.jetbrains.kotlin.idea.KotlinLanguage
2518
import org.jetbrains.kotlin.lexer.KtKeywordToken
2619
import org.jetbrains.kotlin.lexer.KtToken
2720
import org.jetbrains.kotlin.psi.KtAnnotated
@@ -649,27 +642,7 @@ public fun ASTNode.dummyPsiElement(): PsiElement =
649642
}
650643
}
651644

652-
private fun createDummyKtFile(): KtFile {
653-
val disposable = Disposer.newDisposable()
654-
try {
655-
val project =
656-
KotlinCoreEnvironment
657-
.createForProduction(
658-
disposable,
659-
CompilerConfiguration(),
660-
EnvironmentConfigFiles.JVM_CONFIG_FILES,
661-
).project as MockProject
662-
663-
return PsiFileFactory
664-
.getInstance(project)
665-
.createFileFromText("dummy-file.kt", KotlinLanguage.INSTANCE, "") as KtFile
666-
} finally {
667-
// Dispose explicitly to (possibly) prevent memory leak
668-
// https://discuss.kotlinlang.org/t/memory-leak-in-kotlincoreenvironment-and-kotlintojvmbytecodecompiler/21950
669-
// https://youtrack.jetbrains.com/issue/KT-47044
670-
disposable.dispose()
671-
}
672-
}
645+
private fun createDummyKtFile(): KtFile = KtlintKotlinCompiler.createPsiFileFromText("File.kt", "") as KtFile
673646

674647
/**
675648
* Returns true if the receiver is not null, and it represents a declaration

ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/KtlintKotlinCompiler.kt renamed to ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/KtlintKotlinCompiler.kt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
package com.pinterest.ktlint.rule.engine.internal
1+
package com.pinterest.ktlint.rule.engine.core.api
22

3+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.BLOCK
4+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.SCRIPT
5+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.SCRIPT_INITIALIZER
36
import com.pinterest.ktlint.rule.engine.core.util.cast
47
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
58
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
69
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
10+
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
711
import org.jetbrains.kotlin.com.intellij.mock.MockProject
812
import org.jetbrains.kotlin.com.intellij.openapi.diagnostic.DefaultLogger
913
import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer
@@ -24,16 +28,28 @@ import org.jetbrains.kotlin.com.intellij.openapi.diagnostic.Logger as Diagnostic
2428
/**
2529
* Embedded Kotlin Compiler configured for use by Ktlint.
2630
*/
27-
internal object KtlintKotlinCompiler {
31+
public object KtlintKotlinCompiler {
2832
private val psiFileFactory = initPsiFileFactory()
2933

3034
/**
3135
* Create a PSI file with name [psiFileName] and content [text].
3236
*/
33-
fun createPsiFileFromText(
37+
public fun createPsiFileFromText(
3438
psiFileName: String,
3539
text: String,
3640
): PsiFile = psiFileFactory.createFileFromText(psiFileName, KotlinLanguage.INSTANCE, text)
41+
42+
/**
43+
* Create the AST for a given piece of code.
44+
*/
45+
public fun createASTNodeFromText(text: String): ASTNode? =
46+
// For a code snippet which is not necessarily compilable if it was compiled as a standalone file, it is better to compile it as
47+
// kotlin script.
48+
createPsiFileFromText("File.kts", text)
49+
.node
50+
.findChildByType(SCRIPT)
51+
?.findChildByType(BLOCK)
52+
?.let { it.findChildByType(SCRIPT_INITIALIZER) ?: it }
3753
}
3854

3955
/**

ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/KtlintSuppression.kt

Lines changed: 27 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package com.pinterest.ktlint.rule.engine.internal
22

33
import com.pinterest.ktlint.rule.engine.core.api.ElementType
4+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATED_EXPRESSION
5+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATION_ENTRY
46
import com.pinterest.ktlint.rule.engine.core.api.ElementType.COMMA
57
import com.pinterest.ktlint.rule.engine.core.api.ElementType.FILE
8+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUN
9+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.MODIFIER_LIST
610
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_ARGUMENT_LIST
711
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_PARAMETER
812
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_PARAMETER_LIST
@@ -11,6 +15,7 @@ import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT
1115
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT_LIST
1216
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER
1317
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER_LIST
18+
import com.pinterest.ktlint.rule.engine.core.api.KtlintKotlinCompiler
1419
import com.pinterest.ktlint.rule.engine.core.api.findChildByTypeRecursively
1520
import com.pinterest.ktlint.rule.engine.core.api.firstChildLeafOrSelf
1621
import com.pinterest.ktlint.rule.engine.core.api.indent
@@ -21,33 +26,26 @@ import com.pinterest.ktlint.rule.engine.core.api.nextCodeSibling
2126
import com.pinterest.ktlint.rule.engine.core.api.replaceWith
2227
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
2328
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
24-
import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory
2529
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl
2630
import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet
27-
import org.jetbrains.kotlin.idea.KotlinLanguage
2831
import org.jetbrains.kotlin.psi.KtAnnotatedExpression
2932
import org.jetbrains.kotlin.psi.KtAnnotationEntry
3033
import org.jetbrains.kotlin.psi.KtBinaryExpression
3134
import org.jetbrains.kotlin.psi.KtBlockExpression
3235
import org.jetbrains.kotlin.psi.KtClass
3336
import org.jetbrains.kotlin.psi.KtClassInitializer
3437
import org.jetbrains.kotlin.psi.KtDeclaration
35-
import org.jetbrains.kotlin.psi.KtDeclarationModifierList
3638
import org.jetbrains.kotlin.psi.KtExpression
3739
import org.jetbrains.kotlin.psi.KtFile
3840
import org.jetbrains.kotlin.psi.KtFileAnnotationList
3941
import org.jetbrains.kotlin.psi.KtFunction
4042
import org.jetbrains.kotlin.psi.KtFunctionLiteral
4143
import org.jetbrains.kotlin.psi.KtLambdaExpression
42-
import org.jetbrains.kotlin.psi.KtNamedFunction
4344
import org.jetbrains.kotlin.psi.KtPrimaryConstructor
4445
import org.jetbrains.kotlin.psi.KtProperty
4546
import org.jetbrains.kotlin.psi.KtPropertyAccessor
46-
import org.jetbrains.kotlin.psi.KtScript
47-
import org.jetbrains.kotlin.psi.KtScriptInitializer
4847
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
4948
import org.jetbrains.kotlin.psi.psiUtil.children
50-
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
5149
import org.jetbrains.kotlin.util.prefixIfNot
5250

5351
private const val KTLINT_PREFIX = "ktlint"
@@ -273,19 +271,15 @@ private fun ASTNode.createSuppressAnnotation(
273271
val fileAnnotation = targetNode.createFileAnnotation(suppressType, suppressions)
274272
this.replaceWith(fileAnnotation.firstChildNode)
275273
} else {
276-
val modifierListWithAnnotation = targetNode.createModifierListWithAnnotationEntry(suppressType, suppressions)
277-
this.replaceWith(
278-
modifierListWithAnnotation
279-
.getChildOfType<KtAnnotationEntry>()!!
280-
.node,
281-
)
274+
val modifierListWithAnnotation = createModifierListWithAnnotationEntry(suppressType, suppressions)
275+
this.replaceWith(modifierListWithAnnotation.findChildByType(ANNOTATION_ENTRY)!!)
282276
}
283277
}
284278

285279
is KtClass, is KtFunction, is KtProperty, is KtPropertyAccessor -> {
286280
this.addChild(PsiWhiteSpaceImpl(indent()), this.firstChildNode)
287-
val modifierListWithAnnotation = targetNode.createModifierListWithAnnotationEntry(suppressType, suppressions)
288-
this.addChild(modifierListWithAnnotation.node, this.firstChildNode)
281+
val modifierListWithAnnotation = createModifierListWithAnnotationEntry(suppressType, suppressions)
282+
this.addChild(modifierListWithAnnotation, this.firstChildNode)
289283
}
290284

291285
else -> {
@@ -294,15 +288,10 @@ private fun ASTNode.createSuppressAnnotation(
294288
this.elementType != VALUE_PARAMETER
295289
) {
296290
val annotatedExpression = targetNode.createAnnotatedExpression(suppressType, suppressions)
297-
treeParent.replaceChild(targetNode, annotatedExpression.node)
291+
treeParent.replaceChild(targetNode, annotatedExpression)
298292
} else {
299-
val modifierListWithAnnotation = targetNode.createModifierListWithAnnotationEntry(suppressType, suppressions)
300-
treeParent.addChild(
301-
modifierListWithAnnotation
302-
.getChildOfType<KtAnnotationEntry>()!!
303-
.node,
304-
this,
305-
)
293+
val modifierListWithAnnotation = createModifierListWithAnnotationEntry(suppressType, suppressions)
294+
treeParent.addChild(modifierListWithAnnotation.findChildByType(ANNOTATION_ENTRY)!!, this)
306295
treeParent.addChild(PsiWhiteSpaceImpl(indent()), this)
307296
}
308297
}
@@ -318,12 +307,12 @@ private fun ASTNode.createFileAnnotation(
318307
.joinToString()
319308
.let { sortedSuppressions -> "@file:${suppressType.annotationName}($sortedSuppressions)" }
320309
.let { annotation ->
321-
PsiFileFactory
322-
.getInstance(psi.project)
323-
.createFileFromText(KotlinLanguage.INSTANCE, annotation)
324-
?.firstChild
310+
KtlintKotlinCompiler
311+
.createPsiFileFromText("file.kt", annotation)
312+
.firstChild
313+
?.node
325314
?: throw IllegalStateException("Can not create annotation '$annotation'")
326-
}.node
315+
}
327316

328317
private fun ASTNode.createFileAnnotationList(annotation: ASTNode) {
329318
require(isRoot()) { "File annotation list can only be created for root node" }
@@ -340,52 +329,43 @@ private fun ASTNode.createFileAnnotationList(annotation: ASTNode) {
340329
}
341330
}
342331

343-
private fun ASTNode.createModifierListWithAnnotationEntry(
332+
private fun createModifierListWithAnnotationEntry(
344333
suppressType: SuppressAnnotationType,
345334
suppressions: Set<String>,
346-
): PsiElement =
335+
): ASTNode =
347336
suppressions
348337
.sorted()
349338
.joinToString()
350339
.let { sortedSuppressions -> "@${suppressType.annotationName}($sortedSuppressions)" }
351340
.let { annotation ->
352-
PsiFileFactory
353-
.getInstance(psi.project)
354-
.createFileFromText(
355-
KotlinLanguage.INSTANCE,
341+
KtlintKotlinCompiler
342+
.createASTNodeFromText(
356343
// Create the annotation for a dummy declaration as the entire code block should be valid Kotlin code
357344
"""
358345
$annotation
359346
fun foo() {}
360347
""".trimIndent(),
361-
).getChildOfType<KtScript>()
362-
?.getChildOfType<KtBlockExpression>()
363-
?.getChildOfType<KtNamedFunction>()
364-
?.getChildOfType<KtDeclarationModifierList>()
348+
)?.findChildByType(FUN)
349+
?.findChildByType(MODIFIER_LIST)
365350
?: throw IllegalStateException("Can not create annotation '$annotation'")
366351
}
367352

368353
private fun ASTNode.createAnnotatedExpression(
369354
suppressType: SuppressAnnotationType,
370355
suppressions: Set<String>,
371-
): PsiElement =
356+
): ASTNode =
372357
suppressions
373358
.sorted()
374359
.joinToString()
375360
.let { sortedSuppressions -> "@${suppressType.annotationName}($sortedSuppressions)" }
376361
.let { annotation ->
377-
PsiFileFactory
378-
.getInstance(psi.project)
379-
.createFileFromText(
380-
KotlinLanguage.INSTANCE,
362+
KtlintKotlinCompiler
363+
.createASTNodeFromText(
381364
"""
382365
|${this.indent(false)}$annotation
383366
|${this.indent(false)}${this.text}
384367
""".trimMargin(),
385-
).getChildOfType<KtScript>()
386-
?.getChildOfType<KtBlockExpression>()
387-
?.getChildOfType<KtScriptInitializer>()
388-
?.getChildOfType<KtAnnotatedExpression>()
368+
)?.findChildByType(ANNOTATED_EXPRESSION)
389369
?: throw IllegalStateException("Can not create annotation '$annotation'")
390370
}
391371

ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleExecutionContext.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine
77
import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine.Companion.UTF8_BOM
88
import com.pinterest.ktlint.rule.engine.api.KtLintRuleException
99
import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision
10+
import com.pinterest.ktlint.rule.engine.core.api.KtlintKotlinCompiler
1011
import com.pinterest.ktlint.rule.engine.core.api.Rule
1112
import com.pinterest.ktlint.rule.engine.core.api.RuleAutocorrectApproveHandler
1213
import com.pinterest.ktlint.rule.engine.core.api.RuleId

ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/KtlintSuppressionRule.kt

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ import com.pinterest.ktlint.rule.engine.core.api.ElementType
55
import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATION
66
import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATION_ENTRY
77
import com.pinterest.ktlint.rule.engine.core.api.ElementType.BLOCK_COMMENT
8+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.CALL_EXPRESSION
89
import com.pinterest.ktlint.rule.engine.core.api.ElementType.EOL_COMMENT
10+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.LITERAL_STRING_TEMPLATE_ENTRY
911
import com.pinterest.ktlint.rule.engine.core.api.ElementType.STRING_TEMPLATE
1012
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT
13+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT_LIST
1114
import com.pinterest.ktlint.rule.engine.core.api.IgnoreKtlintSuppressions
15+
import com.pinterest.ktlint.rule.engine.core.api.KtlintKotlinCompiler
1216
import com.pinterest.ktlint.rule.engine.core.api.RuleId
1317
import com.pinterest.ktlint.rule.engine.core.api.findChildByTypeRecursively
1418
import com.pinterest.ktlint.rule.engine.core.api.ifAutocorrectAllowed
@@ -32,20 +36,8 @@ import com.pinterest.ktlint.rule.engine.internal.rules.KtLintDirective.Suppressi
3236
import com.pinterest.ktlint.rule.engine.internal.rules.KtLintDirective.SuppressionIdChange.ValidSuppressionId.Companion.KTLINT_SUPPRESSION_ALL
3337
import com.pinterest.ktlint.rule.engine.internal.toFullyQualifiedKtlintSuppressionId
3438
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
35-
import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory
3639
import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet
37-
import org.jetbrains.kotlin.idea.KotlinLanguage
38-
import org.jetbrains.kotlin.psi.KtBlockExpression
39-
import org.jetbrains.kotlin.psi.KtCallExpression
40-
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry
41-
import org.jetbrains.kotlin.psi.KtScript
42-
import org.jetbrains.kotlin.psi.KtScriptInitializer
43-
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
44-
import org.jetbrains.kotlin.psi.KtValueArgument
45-
import org.jetbrains.kotlin.psi.KtValueArgumentList
46-
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
4740
import org.jetbrains.kotlin.psi.psiUtil.siblings
48-
import org.jetbrains.kotlin.psi.psiUtil.startOffset
4941
import org.jetbrains.kotlin.utils.addToStdlib.applyIf
5042

5143
/**
@@ -120,8 +112,7 @@ public class KtlintSuppressionRule(
120112
} else if (prefixedSuppression != literalStringTemplateEntry.text) {
121113
emit(offset, "Identifier to suppress ktlint rule must be fully qualified with the rule set id", true)
122114
.ifAutocorrectAllowed {
123-
node
124-
.createLiteralStringTemplateEntry(prefixedSuppression)
115+
createLiteralStringTemplateEntry(prefixedSuppression)
125116
?.let { literalStringTemplateEntry.replaceWith(it) }
126117
}
127118
}
@@ -134,19 +125,14 @@ public class KtlintSuppressionRule(
134125
?.remove()
135126
}
136127

137-
private fun ASTNode.createLiteralStringTemplateEntry(prefixedSuppression: String) =
138-
PsiFileFactory
139-
.getInstance(psi.project)
140-
.createFileFromText(KotlinLanguage.INSTANCE, "listOf(\"$prefixedSuppression\")")
141-
.getChildOfType<KtScript>()
142-
?.getChildOfType<KtBlockExpression>()
143-
?.getChildOfType<KtScriptInitializer>()
144-
?.getChildOfType<KtCallExpression>()
145-
?.getChildOfType<KtValueArgumentList>()
146-
?.getChildOfType<KtValueArgument>()
147-
?.getChildOfType<KtStringTemplateExpression>()
148-
?.getChildOfType<KtLiteralStringTemplateEntry>()
149-
?.node
128+
private fun createLiteralStringTemplateEntry(prefixedSuppression: String) =
129+
KtlintKotlinCompiler
130+
.createASTNodeFromText("listOf(\"$prefixedSuppression\")")
131+
?.findChildByType(CALL_EXPRESSION)
132+
?.findChildByType(VALUE_ARGUMENT_LIST)
133+
?.findChildByType(VALUE_ARGUMENT)
134+
?.findChildByType(STRING_TEMPLATE)
135+
?.findChildByType(LITERAL_STRING_TEMPLATE_ENTRY)
150136

151137
private fun KtLintDirective.visitKtlintDirective(
152138
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision,

0 commit comments

Comments
 (0)