From 016e77ab202ca8086987e515c119d6feb38bc86f Mon Sep 17 00:00:00 2001 From: Bas Leijdekkers Date: Tue, 10 Dec 2019 12:25:38 +0100 Subject: [PATCH] SSR: match enhanced switch statements and switch expressions (IDEA-203910) GitOrigin-RevId: 92b929008336c713597dbaf7d98346b6a89f8b5d --- .../JavaPredefinedConfigurations.java | 4 +- .../JavaStructuralSearchProfile.java | 12 +- .../impl/matcher/JavaMatchingVisitor.java | 117 ++++++++++++------ .../compiler/JavaCompilingVisitor.java | 33 +++-- .../intellij/structuralsearch/Matcher.java | 10 +- .../source/messages/SSRBundle.properties | 2 +- .../JavaPredefinedConfigurationsTest.java | 19 +++ .../StructuralSearchTest.java | 63 +++++++++- 8 files changed, 188 insertions(+), 72 deletions(-) diff --git a/java/structuralsearch-java/src/com/intellij/structuralsearch/JavaPredefinedConfigurations.java b/java/structuralsearch-java/src/com/intellij/structuralsearch/JavaPredefinedConfigurations.java index 7f642e1c582d..bd8dd0cf0e2d 100644 --- a/java/structuralsearch-java/src/com/intellij/structuralsearch/JavaPredefinedConfigurations.java +++ b/java/structuralsearch-java/src/com/intellij/structuralsearch/JavaPredefinedConfigurations.java @@ -335,7 +335,9 @@ class JavaPredefinedConfigurations { createSearchTemplateInfo(SSRBundle.message("predefined.configuration.any.boxing"), "'_expression:[ exprtype( int|boolean|long|char|short|byte ) && formal( Object|Integer|Boolean|Long|Character|Short|Byte )]", INTERESTING_TYPE), createSearchTemplateInfo(SSRBundle.message("predefined.configuration.any.unboxing"), "'_expression:[ formal( int|boolean|long|char|short|byte ) && exprtype( Integer|Boolean|Long|Character|Short|Byte )]", INTERESTING_TYPE), createSearchTemplateInfo(SSRBundle.message("predefined.configuration.try.without.resources"), "try ('_ResourceType '_resource{0,0} = '_init; '_expression{0,0}) {\n '_TryStatement*;\n} catch('_ExceptionType '_Exception{0,0}) {\n '_CatchStatement*;\n}", INTERESTING_TYPE), - createSearchTemplateInfo(SSRBundle.message("predefined.configuration.switch.with.branches"), "switch ('_expression) {\n case '_value{0,4}\\: '_statement*;\n}", INTERESTING_TYPE), + createSearchTemplateInfo(SSRBundle.message("predefined.configuration.switch.with.branches"), "[ script( \"import com.intellij.psi.*;\n" + + "import com.intellij.psi.util.*;\n" + + "PsiTreeUtil.getChildrenOfType(__context__.body, PsiSwitchLabelStatementBase.class).length < 5\" ) ]switch ('_expression) {\n}", INTERESTING_TYPE), createSearchTemplateInfo(SSRBundle.message("predefined.configuration.labeled.break"), "break '_label;", INTERESTING_TYPE), createSearchTemplateInfo(SSRBundle.message("predefined.configuration.methods.with.final.parameters"), "'_ReturnType? '_Method('_BeforeType '_BeforeParameter*, final '_ParameterType '_Parameter, '_AfterType '_AfterParameter*);", diff --git a/java/structuralsearch-java/src/com/intellij/structuralsearch/JavaStructuralSearchProfile.java b/java/structuralsearch-java/src/com/intellij/structuralsearch/JavaStructuralSearchProfile.java index ef1bbb85ef2e..34b517532e29 100644 --- a/java/structuralsearch-java/src/com/intellij/structuralsearch/JavaStructuralSearchProfile.java +++ b/java/structuralsearch-java/src/com/intellij/structuralsearch/JavaStructuralSearchProfile.java @@ -802,8 +802,7 @@ public class JavaStructuralSearchProfile extends StructuralSearchProfile { parent instanceof PsiClass ? PsiMember.class : PsiJavaCodeReferenceElement.class, buf); } else if (info.isStatementContext() || info.isArgumentContext() || parent instanceof PsiPolyadicExpression) { - addSeparatorText - (previous, currentElement, buf); + addSeparatorText(previous, currentElement, buf); } else { buf.append(" "); // doesn't happen @@ -1050,12 +1049,6 @@ public class JavaStructuralSearchProfile extends StructuralSearchProfile { if (grandParent instanceof PsiNameValuePair) return ((PsiNameValuePair)grandParent).getValue() == parent; if (grandParent instanceof PsiForStatement) return true; } - if (grandParent instanceof PsiExpressionList) { - final PsiElement label = grandParent.getParent(); - if (label instanceof PsiSwitchLabelStatementBase) { - return ((PsiSwitchLabelStatementBase)label).getEnclosingSwitchStatement() != null; - } - } if (grandParent instanceof PsiVariable) { return ((PsiVariable)grandParent).getInitializer() == parent; } @@ -1100,7 +1093,6 @@ public class JavaStructuralSearchProfile extends StructuralSearchProfile { final PsiElement grandParent = parent.getParent(); if (grandParent instanceof PsiPolyadicExpression) return true; - if (grandParent instanceof PsiExpressionList && grandParent.getParent() instanceof PsiSwitchLabelStatementBase) return true; if (grandParent instanceof PsiExpressionStatement && isCompleteStatement((PsiExpressionStatement)grandParent)) { final PsiElement greatGrandParent = grandParent.getParent(); if (greatGrandParent instanceof PsiForStatement) { @@ -1125,7 +1117,7 @@ public class JavaStructuralSearchProfile extends StructuralSearchProfile { if (grandParent instanceof PsiCatchSection && parent instanceof PsiParameter) return true; if (grandParent instanceof PsiAnnotation && !(grandParent.getParent().getNextSibling() instanceof PsiErrorElement)) return true; if (grandParent instanceof PsiParameterList || grandParent instanceof PsiArrayInitializerMemberValue || - (grandParent instanceof PsiExpressionList && !(grandParent.getParent() instanceof PsiSwitchLabelStatementBase)) || + grandParent instanceof PsiExpressionList || grandParent instanceof PsiTypeParameterList || grandParent instanceof PsiResourceList || grandParent instanceof PsiResourceExpression || grandParent instanceof PsiArrayInitializerExpression) { return true; diff --git a/java/structuralsearch-java/src/com/intellij/structuralsearch/impl/matcher/JavaMatchingVisitor.java b/java/structuralsearch-java/src/com/intellij/structuralsearch/impl/matcher/JavaMatchingVisitor.java index 5eb603f9b3dc..bc58c5bc05d5 100644 --- a/java/structuralsearch-java/src/com/intellij/structuralsearch/impl/matcher/JavaMatchingVisitor.java +++ b/java/structuralsearch-java/src/com/intellij/structuralsearch/impl/matcher/JavaMatchingVisitor.java @@ -105,7 +105,7 @@ public class JavaMatchingVisitor extends JavaElementVisitor { final PsiAnnotation[] annotations = list.getAnnotations(); if (annotations.length > 0) { - Set annotationSet = ContainerUtil.set(annotations); + final Set annotationSet = ContainerUtil.set(annotations); for (PsiAnnotation annotation : annotations) { final PsiJavaCodeReferenceElement nameReferenceElement = annotation.getNameReferenceElement(); @@ -1174,51 +1174,88 @@ public class JavaMatchingVisitor extends JavaElementVisitor { @Override public void visitSwitchStatement(PsiSwitchStatement statement) { - final PsiSwitchStatement other = (PsiSwitchStatement)myMatchingVisitor.getElement(); - - if (myMatchingVisitor.setResult(myMatchingVisitor.match(statement.getExpression(), other.getExpression()))) { - final List cases1 = PsiTreeUtil.getChildrenOfTypeAsList(statement.getBody(), PsiSwitchLabelStatement.class); - if (cases1.isEmpty()) { - myMatchingVisitor.setResult(myMatchingVisitor.matchSons(statement.getBody(), other.getBody())); - } - else { - final List cases2 = PsiTreeUtil.getChildrenOfTypeAsList(other.getBody(), PsiSwitchLabelStatement.class); - myMatchingVisitor.setResult( - myMatchingVisitor.matchSequentially(cases1.toArray(PsiElement.EMPTY_ARRAY), cases2.toArray(PsiElement.EMPTY_ARRAY))); - } - } + myMatchingVisitor.setResult(matchSwitchBlock(statement)); } @Override - public void visitSwitchLabelStatement(PsiSwitchLabelStatement case1) { - final PsiSwitchLabelStatement other = (PsiSwitchLabelStatement)myMatchingVisitor.getElement(); - final MatchContext context = myMatchingVisitor.getMatchContext(); - final PsiExpression value1 = case1.getCaseValue(); - final PsiExpression value2 = other.getCaseValue(); - final boolean isTypedVar = context.getPattern().isTypedVar(value1); - context.pushResult(); - try { - if (myMatchingVisitor.setResult(isTypedVar || - case1.isDefaultCase() == other.isDefaultCase() && myMatchingVisitor.match(value1, value2))) { - final List statements = collectCaseStatements(case1); - if (!statements.isEmpty()) { - myMatchingVisitor.setResult(myMatchingVisitor.matchSequentially(statements.toArray(PsiElement.EMPTY_ARRAY), - collectCaseStatements(other).toArray(PsiElement.EMPTY_ARRAY))); - } - } - } finally { - myMatchingVisitor.scopeMatch(value1, isTypedVar, (value2 == null) ? other : value2); - } + public void visitSwitchExpression(PsiSwitchExpression expression) { + myMatchingVisitor.setResult(matchSwitchBlock(expression)); } - private static List collectCaseStatements(PsiSwitchLabelStatement switchLabelStatement) { - final List result = new SmartList<>(); - PsiStatement sibling = PsiTreeUtil.getNextSiblingOfType(switchLabelStatement, PsiStatement.class); - while (sibling != null && !(sibling instanceof PsiSwitchLabelStatement)) { - result.add(sibling); - sibling = PsiTreeUtil.getNextSiblingOfType(sibling, PsiStatement.class); + private boolean matchSwitchBlock(PsiSwitchBlock switchBlock) { + final PsiSwitchBlock other = (PsiSwitchBlock)myMatchingVisitor.getElement(); + + if (!myMatchingVisitor.match(switchBlock.getExpression(), other.getExpression())) { + return false; } - return result; + final PsiCodeBlock body = switchBlock.getBody(); + final PsiSwitchLabelStatementBase[] cases1 = PsiTreeUtil.getChildrenOfType(body, PsiSwitchLabelStatementBase.class); + if (cases1 != null) { + final PsiSwitchLabelStatementBase[] cases2 = PsiTreeUtil.getChildrenOfType(other.getBody(), PsiSwitchLabelStatementBase.class); + return myMatchingVisitor.matchSequentially(cases1, cases2 != null ? cases2 : PsiElement.EMPTY_ARRAY); + } + final List statements1 = PsiTreeUtil.getChildrenOfAnyType(body, PsiStatement.class, PsiComment.class); + if (!statements1.isEmpty()) { + final List statements2 = PsiTreeUtil.getChildrenOfAnyType(other.getBody(), PsiStatement.class, PsiComment.class); + return myMatchingVisitor.matchSequentially(statements1.toArray(PsiElement.EMPTY_ARRAY), statements2.toArray(PsiElement.EMPTY_ARRAY)); + } + return true; + } + + @Override + public void visitSwitchLabelStatement(PsiSwitchLabelStatement statement) { + myMatchingVisitor.setResult(matchLabelStatement(statement, (PsiSwitchLabelStatementBase)myMatchingVisitor.getElement())); + } + + @Override + public void visitSwitchLabeledRuleStatement(PsiSwitchLabeledRuleStatement statement) { + myMatchingVisitor.setResult(matchLabelStatement(statement, (PsiSwitchLabelStatementBase)myMatchingVisitor.getElement())); + } + + private boolean matchLabelStatement(PsiSwitchLabelStatementBase statement1, PsiSwitchLabelStatementBase statement2) { + final PsiExpressionList values1 = statement1.getCaseValues(); + final PsiExpressionList values2 = statement2.getCaseValues(); + if (statement1.isDefaultCase() && !statement2.isDefaultCase()) { + return false; + } + if (values1 == null) { + return true; + } + final PsiExpression[] expressions = (values2 == null) ? PsiExpression.EMPTY_ARRAY : values2.getExpressions(); + if (!myMatchingVisitor.matchInAnyOrder(values1.getExpressions(), expressions)) { + return false; + } + final PsiElement[] body = getBody(statement1); + return body.length == 0 || myMatchingVisitor.matchSequentially(body, getBody(statement2)); + } + + private static PsiElement[] getBody(PsiSwitchLabelStatementBase switchLabelStatement) { + final List result = new SmartList<>(); + if (switchLabelStatement instanceof PsiSwitchLabeledRuleStatement) { + final PsiStatement body = ((PsiSwitchLabeledRuleStatement)switchLabelStatement).getBody(); + if (body instanceof PsiBlockStatement) { + result.addAll(PsiTreeUtil.getChildrenOfAnyType(((PsiBlockStatement)body).getCodeBlock(), PsiStatement.class, PsiComment.class)); + } + else { + result.add(body); + } + } + else { + PsiElement sibling = PsiTreeUtil.getNextSiblingOfType(switchLabelStatement, PsiStatement.class); + while (sibling != null && !(sibling instanceof PsiSwitchLabelStatement)) { + if (sibling instanceof PsiStatement || sibling instanceof PsiComment) { + result.add(sibling); + } + sibling = sibling.getNextSibling(); + } + } + return result.toArray(PsiElement.EMPTY_ARRAY); + } + + @Override + public void visitYieldStatement(PsiYieldStatement statement) { + final PsiYieldStatement other = (PsiYieldStatement)myMatchingVisitor.getElement(); + myMatchingVisitor.setResult(myMatchingVisitor.match(statement.getExpression(), other.getExpression())); } @Override diff --git a/java/structuralsearch-java/src/com/intellij/structuralsearch/impl/matcher/compiler/JavaCompilingVisitor.java b/java/structuralsearch-java/src/com/intellij/structuralsearch/impl/matcher/compiler/JavaCompilingVisitor.java index c871abc87dd2..f637782ee1ec 100644 --- a/java/structuralsearch-java/src/com/intellij/structuralsearch/impl/matcher/compiler/JavaCompilingVisitor.java +++ b/java/structuralsearch-java/src/com/intellij/structuralsearch/impl/matcher/compiler/JavaCompilingVisitor.java @@ -5,7 +5,6 @@ import com.intellij.dupLocator.iterators.NodeIterator; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; -import com.intellij.psi.impl.PsiImplUtil; import com.intellij.psi.impl.source.JavaDummyHolder; import com.intellij.psi.javadoc.PsiDocComment; import com.intellij.psi.javadoc.PsiDocTag; @@ -290,14 +289,6 @@ public class JavaCompilingVisitor extends JavaRecursiveElementWalkingVisitor { // typed var for expression (but not top level) final MatchingHandler handler = myCompilingVisitor.getContext().getPattern().getHandler(reference); GlobalCompilingVisitor.setFilter(handler, ExpressionFilter.getInstance()); - if (handler instanceof SubstitutionHandler) { - final PsiSwitchLabelStatementBase label = PsiImplUtil.getSwitchLabel(reference); - if (label != null) { - final SubstitutionHandler handler1 = (SubstitutionHandler)handler; - pattern.setHandler(label, new SubstitutionHandler("__case_" + label.getTextOffset(), false, - handler1.getMinOccurs(), handler1.getMaxOccurs(), true)); - } - } typedVarProcessed = true; } @@ -339,9 +330,27 @@ public class JavaCompilingVisitor extends JavaRecursiveElementWalkingVisitor { } @Override - public void visitBlockStatement(PsiBlockStatement psiBlockStatement) { - super.visitBlockStatement(psiBlockStatement); - myCompilingVisitor.setFilterSimple(psiBlockStatement, BlockFilter.getInstance()); + public void visitBlockStatement(PsiBlockStatement statement) { + super.visitBlockStatement(statement); + myCompilingVisitor.setFilterSimple(statement, BlockFilter.getInstance()); + } + + @Override + public void visitSwitchStatement(PsiSwitchStatement statement) { + super.visitSwitchStatement(statement); + myCompilingVisitor.setFilterSimple(statement, e -> e instanceof PsiSwitchBlock); + } + + @Override + public void visitSwitchLabelStatement(PsiSwitchLabelStatement statement) { + super.visitSwitchLabelStatement(statement); + myCompilingVisitor.setFilterSimple(statement, e -> e instanceof PsiSwitchLabelStatementBase); + } + + @Override + public void visitSwitchLabeledRuleStatement(PsiSwitchLabeledRuleStatement statement) { + super.visitSwitchLabeledRuleStatement(statement); + myCompilingVisitor.setFilterSimple(statement, e -> e instanceof PsiSwitchLabelStatementBase); } @Override diff --git a/platform/structuralsearch/source/com/intellij/structuralsearch/Matcher.java b/platform/structuralsearch/source/com/intellij/structuralsearch/Matcher.java index 5f465b06f650..a0cf9605fe6d 100644 --- a/platform/structuralsearch/source/com/intellij/structuralsearch/Matcher.java +++ b/platform/structuralsearch/source/com/intellij/structuralsearch/Matcher.java @@ -325,11 +325,13 @@ public class Matcher { final CollectingMatchResultSink sink = new CollectingMatchResultSink(); try { - final PsiElement[] elements = - MatcherImplUtil.createSourceTreeFromText(source, fileContext ? PatternTreeContext.File : PatternTreeContext.Block, - sourceFileType, project, physicalSourceFile); + if (options.getScope() == null) { + final PsiElement[] elements = + MatcherImplUtil.createSourceTreeFromText(source, fileContext ? PatternTreeContext.File : PatternTreeContext.Block, + sourceFileType, project, physicalSourceFile); - options.setScope(new LocalSearchScope(elements)); + options.setScope(new LocalSearchScope(elements)); + } testFindMatches(sink, options); } finally { options.setScope(null); diff --git a/platform/structuralsearch/source/messages/SSRBundle.properties b/platform/structuralsearch/source/messages/SSRBundle.properties index f8988a14f7ad..a47637924f50 100644 --- a/platform/structuralsearch/source/messages/SSRBundle.properties +++ b/platform/structuralsearch/source/messages/SSRBundle.properties @@ -118,7 +118,7 @@ predefined.configuration.comments=comments predefined.configuration.fields_variables.with.given.name.pattern.updated=fields/variables with given name pattern updated predefined.configuration.trys=try's predefined.configuration.try.without.resources=try statements without resources and catch blocks -predefined.configuration.switch.with.branches=switch statements with few branches +predefined.configuration.switch.with.branches=switch statements \\& expressions with few branches predefined.configuration.labeled.break=labeled break statements predefined.configuration.block.dcls=block dcls predefined.configuration.methods.of.the.class=constructors \\& methods diff --git a/platform/structuralsearch/testSource/com/intellij/structuralsearch/JavaPredefinedConfigurationsTest.java b/platform/structuralsearch/testSource/com/intellij/structuralsearch/JavaPredefinedConfigurationsTest.java index 37de1cad3dd7..59afa4a5e189 100644 --- a/platform/structuralsearch/testSource/com/intellij/structuralsearch/JavaPredefinedConfigurationsTest.java +++ b/platform/structuralsearch/testSource/com/intellij/structuralsearch/JavaPredefinedConfigurationsTest.java @@ -241,6 +241,25 @@ public class JavaPredefinedConfigurationsTest extends PredefinedConfigurationsTe " int z(int i) {" + " return i;" + " }"); + doTest(configurationMap.remove(SSRBundle.message("predefined.configuration.switches")), + "class X {{" + + " int i = switch (1) {" + + " default -> {}" + + " }" + + " switch (2) {" + + " case 1,2:" + + " break;" + + " default:" + + " }" + + "}}", + "switch (1) {" + + " default -> {}" + + " }", + "switch (2) {" + + " case 1,2:" + + " break;" + + " default:" + + " }"); //assertTrue((templates.length - configurationMap.size()) + " of " + templates.length + // " existing templates tested. Untested templates: " + configurationMap.keySet(), configurationMap.isEmpty()); } diff --git a/platform/structuralsearch/testSource/com/intellij/structuralsearch/StructuralSearchTest.java b/platform/structuralsearch/testSource/com/intellij/structuralsearch/StructuralSearchTest.java index 52162cd046d6..50a752ae9e3c 100644 --- a/platform/structuralsearch/testSource/com/intellij/structuralsearch/StructuralSearchTest.java +++ b/platform/structuralsearch/testSource/com/intellij/structuralsearch/StructuralSearchTest.java @@ -3,7 +3,11 @@ package com.intellij.structuralsearch; import com.intellij.openapi.fileTypes.LanguageFileType; import com.intellij.openapi.fileTypes.StdFileTypes; +import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.*; +import com.intellij.psi.impl.DebugUtil; +import com.intellij.psi.search.LocalSearchScope; +import com.intellij.psi.util.PsiUtil; import com.intellij.testFramework.PlatformTestUtil; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.NotNull; @@ -17,6 +21,13 @@ import java.util.List; */ @SuppressWarnings("ALL") public class StructuralSearchTest extends StructuralSearchTestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + setLanguageLevel(LanguageLevel.JDK_14); + } + @Override protected int findMatchesCount(@Language("JAVA") String in, String pattern, LanguageFileType fileType) { return super.findMatchesCount(in, pattern, fileType); @@ -2608,7 +2619,7 @@ public class StructuralSearchTest extends StructuralSearchTestCase { assertEquals("MINIMUM ZERO not applicable for b", checkApplicableConstraints(options, getProject())); options.fillSearchCriteria("case '_a* :"); - assertEquals("MINIMUM ZERO not applicable for a", checkApplicableConstraints(options, getProject())); + assertEquals(null, checkApplicableConstraints(options, getProject())); options.fillSearchCriteria("int '_a:* ;"); assertEquals("TEXT HIERARCHY not applicable for a", checkApplicableConstraints(options, getProject())); @@ -3268,8 +3279,8 @@ public class StructuralSearchTest extends StructuralSearchTestCase { " '_st*;" + "}")); assertEquals("should find switch with 2 cases", 2, findMatchesCount(in, "switch ('_a) {" + - " case '_c{2,2} :" + - " '_st*;" + + " case '_c1 :" + + " case '_c2? :" + "}")); assertEquals("should find swith with one case and default", 2, findMatchesCount(in, "switch ('_a) {" + " case '_c :" + @@ -3278,7 +3289,51 @@ public class StructuralSearchTest extends StructuralSearchTestCase { " '_st2*;" + " }")); assertEquals("should find defaults", 3, findMatchesCount(in, "default:")); - assertEquals("should find cases", 8, findMatchesCount(in, "case '_a :")); + assertEquals("should find cases", 5, findMatchesCount(in, "case '_a :")); + assertEquals("should find cases & defaults", 8, findMatchesCount(in, "case '_a? :")); + assertEquals("should match switch containing 2 statements", 3, findMatchesCount(in, "switch ('_x) {" + + " '_st{2,2};" + + "}")); + } + + public void testFindSwitchExpressions() { + final PsiElementFactory factory = JavaPsiFacade.getElementFactory(getProject()); + final String in = "void dummy() {" + + " int j = switch (i) {\n" + + " case 10 -> {\n" + + " System.out.println(10);\n" + + " }\n" + + " default -> {}\n" + + " }\n" + + " int k = switch (i) {\n" + + " case 10 -> {\n" + + " yield 1;\n" + + " }\n" + + " default -> 0;\n" + + " };" + + " int l = switch (i) {" + + " case 1,2,3: " + + " break;" + + " case 5:" + + " break;" + + " }\n" + + "}"; + // hack to generate code of the right language level + // will no longer be necessary when long LanguageLevel.HIGHEST == JDK_14 + // (probably after JDK14 release in March 2020) + final PsiMethod method = factory.createMethodFromText(in, null, LanguageLevel.JDK_14); + + System.out.println(PsiUtil.getLanguageLevel(method)); + System.out.println(DebugUtil.psiToString(method, false)); + options.setScope(new LocalSearchScope(method)); + assertEquals("find expressions & statements", 2, findMatchesCount(null, "switch (i) {" + + " case 10 -> {" + + " '_st;" + + " }" + + " default -> $X$;" + + "}")); + //options.setScope(new LocalSearchScope(method)); + //assertEquals("find yield statement", 1, findMatchesCount(null, "yield '_x;")); } public void testRepeatedVars() {