diff --git a/src/main/java/org/openrewrite/staticanalysis/CombineMergeableIfStatements.java b/src/main/java/org/openrewrite/staticanalysis/CombineMergeableIfStatements.java new file mode 100644 index 000000000..ce450c931 --- /dev/null +++ b/src/main/java/org/openrewrite/staticanalysis/CombineMergeableIfStatements.java @@ -0,0 +1,242 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.staticanalysis; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.Tree; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.style.IntelliJ; +import org.openrewrite.java.style.TabsAndIndentsStyle; +import org.openrewrite.java.tree.Comment; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaSourceFile; +import org.openrewrite.java.tree.Space; +import org.openrewrite.java.tree.Statement; +import org.openrewrite.java.tree.TextComment; +import org.openrewrite.style.Style; + +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +import static java.util.Objects.requireNonNull; + +@EqualsAndHashCode(callSuper = false) +@Value +public class CombineMergeableIfStatements extends Recipe { + + private static final String CONTINUATION_KEY = "continuationAfterLogicalAnd"; + + String displayName = "Mergeable `if` statements should be combined"; + String description = "Mergeable `if` statements should be combined."; + Set tags = singleton("RSPEC-S1066"); + Duration estimatedEffortPerOccurrence = Duration.ofMinutes(2); + + @Override + public TreeVisitor getVisitor() { + return new JavaIsoVisitor() { + @Override + public J.If visitIf(J.If iff, ExecutionContext ctx) { + J.If outerIf = super.visitIf(iff, ctx); + + if (outerIf.getElsePart() == null) { + // thenPart is either a single if or a block with a single if + J.Block outerBlock = null; + J.If innerIf = null; + if (outerIf.getThenPart() instanceof J.If) { + innerIf = (J.If) outerIf.getThenPart(); + } else if (outerIf.getThenPart() instanceof J.Block) { + outerBlock = (J.Block) outerIf.getThenPart(); + List statements = outerBlock.getStatements(); + if (statements.size() == 1 && statements.get(0) instanceof J.If) { + innerIf = (J.If) statements.get(0); + } + } + + if (innerIf != null && innerIf.getElsePart() == null) { + // thenPart of outer if is replaced with thenPart of innerIf + // combine conditions with logical AND : correct parenthesizing is handled by JavaTemplate + Expression outerCondition = outerIf.getIfCondition().getTree(); + Expression innerCondition = innerIf.getIfCondition().getTree(); + + UUID innerIfId = Tree.randomId(); + getCursor().getRoot().putMessage(innerIfId.toString(), innerIf.getComments()); + UUID outerBlockId = Tree.randomId(); + getCursor().getRoot().putMessage(outerBlockId.toString(), + Optional.ofNullable(outerBlock).map(J::getComments).orElse(emptyList())); + + doAfterVisit(new MergedConditionalVisitor<>()); + + J.If merged = JavaTemplate.apply( + String.format("#{any()} /*%s,%s,%s*/&& #{any()}", CONTINUATION_KEY, innerIfId, outerBlockId), + getCursor(), + outerCondition.getCoordinates().replace(), + outerCondition, + innerCondition) + .withThenPart(innerIf.getThenPart()); + return autoFormat(merged, ctx); + } + } + + return outerIf; + } + }; + } + + @RequiredArgsConstructor + private static class MergedConditionalVisitor

extends JavaIsoVisitor

{ + + @Nullable + private TabsAndIndentsStyle tabsAndIndentsStyle; + + @Override + public @Nullable J visit(@Nullable Tree tree, P p) { + if (tree instanceof JavaSourceFile) { + JavaSourceFile cu = (JavaSourceFile) requireNonNull(tree); + tabsAndIndentsStyle = Style.from(TabsAndIndentsStyle.class, cu, IntelliJ::tabsAndIndents); + } + return super.visit(tree, p); + } + + @Override + public Space visitSpace(@Nullable Space space, Space.Location loc, P p) { + Space s = super.visitSpace(space, loc, p); + if (s.getComments().size() == 1 && + s.getComments().get(0) instanceof TextComment) { + TextComment onlyComment = (TextComment) s.getComments().get(0); + if (onlyComment.isMultiline() && + onlyComment.getText().startsWith(CONTINUATION_KEY) && + getCursor().firstEnclosingOrThrow(J.Binary.class).getOperator() == J.Binary.Type.And) { + final String[] arr = onlyComment.getText().split(","); + final String innerIfId = arr[1]; + final String outerBlockId = arr[2]; + List innerIfComments = Optional.ofNullable(getCursor().getRoot().>pollMessage(innerIfId)).orElse(emptyList()); + List outerBlockComments = Optional.ofNullable(getCursor().getRoot().>pollMessage(outerBlockId)).orElse(emptyList()); + + getCursor().putMessageOnFirstEnclosing(J.Binary.class, CONTINUATION_KEY, innerIfComments); + s = s.withComments(outerBlockComments); + } + } + + return s; + } + + @Override + public J.Binary visitBinary(J.Binary binary, P p) { + J.Binary b = super.visitBinary(binary, p); + + List comments = getCursor().pollMessage(CONTINUATION_KEY); + if (comments != null) { + final String outerIfIndent = getCursor().firstEnclosingOrThrow(J.If.class).getPrefix().getIndent(); + final String continuationIndent = continuationIndent(requireNonNull(tabsAndIndentsStyle), outerIfIndent); + + if (comments.isEmpty()) { + b = b.withRight(b.getRight() + .withPrefix(Space.format("\n" + continuationIndent))); + } else { + b = b.withRight(b.getRight() + .withComments(ListUtils.map(comments, c -> replaceIndent(c, continuationIndent)))); + } + } + + return b; + } + + private Comment replaceIndent(Comment comment, String newIndent) { + Comment c = comment.withSuffix(replaceLastLineWithIndent(comment.getSuffix(), newIndent)); + if (c.isMultiline() && c instanceof TextComment) { + TextComment tc = (TextComment) c; + c = tc.withText(replaceTextIndent(tc.getText(), newIndent)); + } + return c; + } + + private String replaceTextIndent(final String text, final String newIndent) { + final StringBuilder sb = new StringBuilder(); + boolean found = false; + for (final char ch : text.toCharArray()) { + if (ch == ' ' || ch == '\t') { + if (!found) { + sb.append(ch); + } + } else if (ch == '\r' || ch == '\n') { + sb.append(ch); + found = true; + } else { + if (found) { + sb.append(newIndent); + if (ch == '*') { + sb.append(' '); + } + } + sb.append(ch); + found = false; + } + } + if (found) { + sb.append(newIndent); + sb.append(' '); + } + return sb.toString(); + } + + private String replaceLastLineWithIndent(String whitespace, String indent) { + int idx = whitespace.length() - 1; + while (idx >= 0) { + char c = whitespace.charAt(idx); + if (c == '\r' || c == '\n') { + break; + } + idx--; + } + if (idx >= 0) { + return whitespace.substring(0, idx + 1) + indent; + } + return whitespace; + } + + private String continuationIndent(TabsAndIndentsStyle tabsAndIndents, String currentIndent) { + char c; + int len; + if (tabsAndIndents.getUseTabCharacter()) { + c = '\t'; + len = tabsAndIndents.getContinuationIndent() / tabsAndIndents.getTabSize(); + } else { + c = ' '; + len = tabsAndIndents.getContinuationIndent(); + } + + StringBuilder sb = new StringBuilder(currentIndent); + for (int i = 0; i < len; i++) { + sb.append(c); + } + return sb.toString(); + } + } +} diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index 24735bf98..bb5b954e5 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -20,6 +20,7 @@ maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanaly maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.CatchClauseOnlyRethrows,Catch clause should do more than just rethrow,A `catch` clause that only rethrows the caught exception is unnecessary. Letting the exception bubble up as normal achieves the same result with less code.,1,,Static analysis and remediation,,Remediations for issues identified by SAST tools., maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.ChainStringBuilderAppendCalls,Chain `StringBuilder.append()` calls,"String concatenation within calls to `StringBuilder.append()` causes unnecessary memory allocation. Except for concatenations of String literals, which are joined together at compile time. Replaces inefficient concatenations with chained calls to `StringBuilder.append()`.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools., maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.CollectionToArrayShouldHaveProperType,'Collection.toArray()' should be passed an array of the proper type,"Using `Collection.toArray()` without parameters returns an `Object[]`, which requires casting. It is more efficient and clearer to use `Collection.toArray(new T[0])` instead.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools., +maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.CombineMergeableIfStatements,Mergeable `if` statements should be combined,Mergeable `if` statements should be combined.,1,,Static analysis and remediation,,Remediations for issues identified by SAST tools., maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.CombineSemanticallyEqualCatchBlocks,Combine semantically equal catch blocks,Combine catches in a try that contain semantically equivalent blocks. No change will be made when a caught exception exists if combining catches may change application behavior or type attribution is missing.,1,,Static analysis and remediation,,Remediations for issues identified by SAST tools., maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.CompareEnumsWithEqualityOperator,Enum values should be compared with "==",Replaces `Enum equals(java.lang.Object)` with `Enum == java.lang.Object`. An `!Enum equals(java.lang.Object)` will change to `!=`.,1,,Static analysis and remediation,,Remediations for issues identified by SAST tools., maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.ControlFlowIndentation,Control flow statement indentation,"Program flow control statements like `if`, `while`, and `for` can omit curly braces when they apply to only a single statement. This recipe ensures that any statements which follow that statement are correctly indented to show they are not part of the flow control statement.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools., diff --git a/src/test/java/org/openrewrite/staticanalysis/CombineMergeableIfStatementsTest.java b/src/test/java/org/openrewrite/staticanalysis/CombineMergeableIfStatementsTest.java new file mode 100644 index 000000000..11245d624 --- /dev/null +++ b/src/test/java/org/openrewrite/staticanalysis/CombineMergeableIfStatementsTest.java @@ -0,0 +1,672 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.staticanalysis; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.Tree; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.style.TabsAndIndentsStyle; +import org.openrewrite.style.NamedStyles; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static java.util.Collections.emptySet; +import static java.util.Collections.singletonList; +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.version; + +class CombineMergeableIfStatementsTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new CombineMergeableIfStatements()); + } + + @DocumentExample + @Test + void combineMergeableIfStatements() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1) { + if (condition2) { + System.out.println("OK"); + } + } + } + } + """, + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1 && + condition2) { + System.out.println("OK"); + } + } + } + """ + ) + ); + } + + @Test + void simplifyWithPatternMatchingForInstanceOf() { + rewriteRun( + spec -> spec + .recipes(new InstanceOfPatternMatch(), new CombineMergeableIfStatements()) + .allSources(sourceSpec -> version(sourceSpec, 17)), + // language=java + java( + """ + class A { + void a(Object o) { + if (o instanceof String) { + String s = (String) o; + if (s.isEmpty()) { + System.out.println("OK"); + } + } + } + } + """, + """ + class A { + void a(Object o) { + if (o instanceof String s && + s.isEmpty()) { + System.out.println("OK"); + } + } + } + """ + ) + ); + + } + + @Test + void simplifyWithMultiplePatternMatchingForInstanceOf() { + // This test doesn't fully simplify but could with an 'Inline Local Variable Used Once' recipe + rewriteRun( + spec -> spec + .recipes(new InstanceOfPatternMatch(), new CombineMergeableIfStatements()) + .allSources(sourceSpec -> version(sourceSpec, 17)), + // language=java + java( + """ + import java.util.List; + + class A { + void a(Object o1) { + if (o1 instanceof List) { + List list = (List) o1; + if (!list.isEmpty()) { + Object o2 = list.get(0); + if (o2 instanceof String) { + String s = (String) o2; + if (s.isEmpty()) { + System.out.println("OK"); + } + } + } + } + } + } + """, + """ + import java.util.List; + + class A { + void a(Object o1) { + if (o1 instanceof List list && + !list.isEmpty()) { + Object o2 = list.get(0); + if (o2 instanceof String s && + s.isEmpty()) { + System.out.println("OK"); + } + } + } + } + """ + ) + ); + } + + @Test + void combineWithoutBlocks() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1) + if (condition2) + System.out.println("OK"); + } + } + """, + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1 && + condition2) + System.out.println("OK"); + } + } + """ + ) + ); + } + + @Test + void combineSeveralNestedIfs() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) { + if (b1) { + if (b2) { + if (b3) { + if (b4) { + if (b5) { + if (b6) { + System.out.println("OK"); + } + } + } + } + } + } + } + } + """, + """ + class A { + void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) { + if (b1 && + b2 && + b3 && + b4 && + b5 && + b6) { + System.out.println("OK"); + } + } + } + """ + ) + ); + } + + @Test + void combineSeveralNestedIfsWithLineComments() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) { + // Comment 1.0 + if (b1) { // Comment 1 + if (b2) { // Comment 2 + // Comment 3.0 + if (b3) { // Comment 3 + // Comment 4.0 + if (b4) { // Comment 4 + // Comment 5.0 + if (b5) { // Comment 5 + // Comment 6.0 + // Comment 6.1 + // Comment 6.2 + if (b6) { // Comment 6 + System.out.println("OK"); + } + } + } + } + } + } + } + } + """, + """ + class A { + void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) { + // Comment 1.0 + if (b1 && // Comment 1 + b2 && // Comment 2 + // Comment 3.0 + b3 && // Comment 3 + // Comment 4.0 + b4 && // Comment 4 + // Comment 5.0 + b5 && // Comment 5 + // Comment 6.0 + // Comment 6.1 + // Comment 6.2 + b6) { // Comment 6 + System.out.println("OK"); + } + } + } + """ + ) + ); + } + + @Test + void combineSeveralNestedIfsWithMultilineComments() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) { + /* Comment 1.0 */ + if (b1) { /* Comment 1 */ + if (b2) { /* Comment 2 */ + /* Comment 3.0 */ + if (b3) { /* Comment 3 */ + /* Comment 4.0 */ + if (b4) { /* + * Comment 4 + */ + /* + * Comment 5.0 + Comment 5.1 + */ + if (b5) { /* Comment 5 */ + /** Comment 6.0 */ + /** + * Comment 6.1 + Comment 6.2 + */ + if (b6) { /* Comment 6 */ + System.out.println("OK"); + } + } + } + } + } + } + } + } + """, + """ + class A { + void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) { + /* Comment 1.0 */ + if (b1 && /* Comment 1 */ + b2 && /* Comment 2 */ + /* Comment 3.0 */ + b3 && /* Comment 3 */ + /* Comment 4.0 */ + b4 && /* + * Comment 4 + */ + /* + * Comment 5.0 + Comment 5.1 + */ + b5 && /* Comment 5 */ + /** Comment 6.0 */ + /** + * Comment 6.1 + Comment 6.2 + */ + b6) { /* Comment 6 */ + System.out.println("OK"); + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeWhenOuterIfHasElsePart() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1) { + if (condition2) { + System.out.println("OK"); + } + } else { + System.out.println("KO"); + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeWhenOuterIfHasEmptyBlockAsElsePart() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1) { + if (condition2) { + System.out.println("OK"); + } + } else { + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeWhenOuterIfHasEmptyStatementAsElsePart() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1) { + if (condition2) { + System.out.println("OK"); + } + } else; + } + } + """ + ) + ); + } + + @Test + void doNotChangeWhenOuterIfHasOneStatementInThenPartButIsNotIf() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1) { + System.out.println("KO"); + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeWhenOuterIfHasOneStatementWithoutBlockInThenPartButIsNotIf() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1) + System.out.println("KO"); + } + } + """ + ) + ); + } + + @Test + void doNotChangeWhenOuterIfHasTwoStatementsInThenPart() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1) { + if (condition2) { + System.out.println("OK"); + } + System.out.println("KO"); + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeWhenInnerIfHasElsePart() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1) { + if (condition2) { + System.out.println("OK"); + } else { + System.out.println("KO"); + } + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeWhenInnerIfHasEmptyBlockAsElsePart() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1) { + if (condition2) { + System.out.println("OK"); + } else { + } + } + } + } + """ + ) + ); + } + + @Test + void doNotChangeWhenInnerIfHasEmptyStatementAsElsePart() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1) { + if (condition2) { + System.out.println("OK"); + } else; + } + } + } + """ + ) + ); + } + + @Test + void combineMergeableIfStatementsWithComments() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean condition1, boolean condition2) { + // Comment 1.0 + if (condition1) /* Comment 1.1 */ + /* Comment 1.2 */ // Comment 1.3 + /* Comment 1.4 */ { // Comment 2.0 + // Comment 2.1 + /* + * Comment 2.2 + */ // Comment 2.3 + if (condition2) /* Comment 3 */ { // Comment 4 + System.out.println("OK"); + } + } + } + } + """, + """ + class A { + void a(boolean condition1, boolean condition2) { + // Comment 1.0 + if (condition1 /* Comment 1.1 */ + /* Comment 1.2 */ // Comment 1.3 + /* Comment 1.4 */ && // Comment 2.0 + // Comment 2.1 + /* + * Comment 2.2 + */ // Comment 2.3 + condition2) /* Comment 3 */ { // Comment 4 + System.out.println("OK"); + } + } + } + """ + ) + ); + } + + @Test + void combineBinaryConditions() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean condition1, boolean condition2, boolean condition3) { + if (condition1) { + if (condition2 || condition3) { + System.out.println("OK"); + } + } + } + } + """, + """ + class A { + void a(boolean condition1, boolean condition2, boolean condition3) { + if (condition1 && + (condition2 || condition3)) { + System.out.println("OK"); + } + } + } + """ + ) + ); + } + + @Test + void combineLogicalAndConditions() { + rewriteRun( + // language=java + java( + """ + class A { + void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) { + if (b1 && b2) { + if (b3 && + b4) { + if (b5 && b6) { + System.out.println("OK"); + } + } + } + } + } + """, + """ + class A { + void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) { + if (b1 && b2 && + b3 && + b4 && + b5 && b6) { + System.out.println("OK"); + } + } + } + """ + ) + ); + } + + @Test + void combineMergeableIfStatementsWithTwoSpaceIndent() { + rewriteRun( + spec -> spec.parser(JavaParser.fromJavaVersion().styles(singletonList( + new NamedStyles(Tree.randomId(), "test", "test", "test", emptySet(), + singletonList(new TabsAndIndentsStyle(false, 2, 2, 4, false)))))), + // language=java + java( + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1) { + if (condition2) { + System.out.println("OK"); + } + } + } + } + """, + """ + class A { + void a(boolean condition1, boolean condition2) { + if (condition1 && + condition2) { + System.out.println("OK"); + } + } + } + """ + ) + ); + } +}