SSR: remove "context match" difference between test & actual search

Fixes some failing cases of target replacement (IDEA-146380)
This commit is contained in:
Bas Leijdekkers
2018-02-26 20:28:36 +01:00
parent 16afb2dde6
commit 2b0b340c46
13 changed files with 98 additions and 111 deletions
@@ -1,4 +1,4 @@
// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.refactoring.typeMigration.inspections;
import com.intellij.codeInsight.intention.impl.AddOnDemandStaticImportAction;
@@ -169,7 +169,7 @@ public class MigrateAssertToMatcherAssertInspection extends AbstractBaseJavaLoca
return null;
}
final boolean hasMessage = hasMessage(method);
final String searchTemplate = "'Assert*." + method.getName() + "(" + (hasMessage ? "$msg$, " : "") + templatePair.getFirst() + ")";
final String searchTemplate = "'_Assert?." + method.getName() + "(" + (hasMessage ? "$msg$, " : "") + templatePair.getFirst() + ")";
final PsiClass containingClass = method.getContainingClass();
LOG.assertTrue(containingClass != null);
final String qualifier = containingClass.getQualifiedName();
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.refactoring.typeMigration.rules.guava;
import com.intellij.codeInspection.java18StreamApi.PseudoLambdaReplaceTemplate;
@@ -137,9 +123,9 @@ public class GuavaFluentIterableConversionRule extends BaseGuavaTypeConversionRu
PsiType conversionType = null;
boolean needSpecifyType = true;
if (methodName.equals("of")) {
descriptorBase = new TypeConversionDescriptor("'FluentIterable*.of($arr$)", "java.util.Arrays.stream($arr$)");
descriptorBase = new TypeConversionDescriptor("'_FluentIterable?.of($arr$)", "java.util.Arrays.stream($arr$)");
} else if (methodName.equals("from")) {
descriptorBase = new TypeConversionDescriptor("'FluentIterable*.from($it$)", null) {
descriptorBase = new TypeConversionDescriptor("'_FluentIterable?.from($it$)", null) {
@Override
public PsiExpression replace(PsiExpression expression, @NotNull TypeEvaluator evaluator) {
final PsiMethodCallExpression methodCall = (PsiMethodCallExpression)expression;
@@ -277,7 +263,7 @@ public class GuavaFluentIterableConversionRule extends BaseGuavaTypeConversionRu
if (list.getParametersCount() != 1) return null;
final PsiType parameterType = list.getParameters()[0].getType();
if (parameterType instanceof PsiEllipsisType) {
return new TypeConversionDescriptor("$q$.append('params*)", "java.util.stream.Stream.concat($q$, java.util.Arrays.asList($params$).stream())");
return new TypeConversionDescriptor("$q$.append('_params*)", "java.util.stream.Stream.concat($q$, java.util.Arrays.asList($params$).stream())");
}
else if (parameterType instanceof PsiClassType) {
final PsiClass psiClass = PsiTypesUtil.getPsiClass(parameterType);
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.refactoring.typeMigration.rules.guava;
import com.intellij.openapi.diagnostic.Logger;
@@ -129,7 +115,7 @@ public class GuavaOptionalConversionRule extends BaseGuavaTypeConversionRule {
@Override
protected void fillSimpleDescriptors(Map<String, TypeConversionDescriptorBase> descriptorsMap) {
descriptorsMap.put("absent", new TypeConversionDescriptor("'Optional*.absent()", "java.util.Optional.empty()") {
descriptorsMap.put("absent", new TypeConversionDescriptor("'_Optional?.absent()", "java.util.Optional.empty()") {
@Override
public PsiExpression replace(PsiExpression expression, @NotNull TypeEvaluator evaluator) {
LOG.assertTrue(expression instanceof PsiMethodCallExpression);
@@ -146,9 +132,9 @@ public class GuavaOptionalConversionRule extends BaseGuavaTypeConversionRule {
});
descriptorsMap.put("of", new TypeConversionDescriptor("'Optional*.of($ref$)", "java.util.Optional.of($ref$)"));
descriptorsMap.put("fromNullable", new TypeConversionDescriptor("'Optional*.fromNullable($ref$)", "java.util.Optional.ofNullable($ref$)"));
descriptorsMap.put("presentInstances", new TypeConversionDescriptor("'Optional*.presentInstances($it$)", "java.util.stream.StreamSupport.stream($it$.spliterator(), false).map(java.util.Optional::get).collect(java.util.Collectors.toList())"));
descriptorsMap.put("of", new TypeConversionDescriptor("'_Optional?.of($ref$)", "java.util.Optional.of($ref$)"));
descriptorsMap.put("fromNullable", new TypeConversionDescriptor("'_Optional?.fromNullable($ref$)", "java.util.Optional.ofNullable($ref$)"));
descriptorsMap.put("presentInstances", new TypeConversionDescriptor("'_Optional?.presentInstances($it$)", "java.util.stream.StreamSupport.stream($it$.spliterator(), false).map(java.util.Optional::get).collect(java.util.Collectors.toList())"));
final TypeConversionDescriptorBase identity = new TypeConversionDescriptorBase();
descriptorsMap.put("get", identity);
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.refactoring.typeMigration.rules.guava;
import com.intellij.openapi.diagnostic.Logger;
@@ -76,7 +62,7 @@ public class GuavaPredicatesUtil {
private final String myReplaceByStringTemplate;
TypeConversionDescriptorWithLocalVariable(String methodName, String replaceByString) {
super("'Predicates*." + methodName + "(" + (methodName.equals("equalTo") ? "$v$" : "") + ")", null);
super("'_Predicates?." + methodName + "(" + (methodName.equals("equalTo") ? "$v$" : "") + ")", null);
myReplaceByStringTemplate = replaceByString;
}
@@ -1,24 +1,9 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.inspections;
import com.intellij.codeInsight.daemon.impl.HighlightInfo;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.actions.CleanupInspectionIntention;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.util.Pair;
import com.intellij.pom.java.LanguageLevel;
@@ -30,7 +15,6 @@ import com.intellij.testFramework.IdeaTestUtil;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.testFramework.builders.JavaModuleFixtureBuilder;
import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase;
import org.junit.Assert;
import java.util.Arrays;
import java.util.List;
@@ -146,14 +146,6 @@ public class MatchOptions implements JDOMExternalizable {
StringToConstraintsTransformer.transformCriteria(criteria, this);
}
public boolean isResultIsContextMatch() {
return resultIsContextMatch;
}
public void setResultIsContextMatch(boolean resultIsContextMatch) {
this.resultIsContextMatch = resultIsContextMatch;
}
public SearchScope getScope() {
return scope;
}
@@ -24,5 +24,7 @@ public abstract class MatchResult {
public abstract boolean hasChildren();
public abstract boolean isScopeMatch();
public abstract boolean isMultipleMatch();
public abstract MatchResult getRoot();
public abstract boolean isTarget();
}
@@ -227,7 +227,7 @@ public class GlobalMatchingVisitor extends AbstractMatchingVisitor {
}
private void dispatchMatched(final List<PsiElement> matchedNodes, MatchResultImpl result) {
if (!matchContext.getOptions().isResultIsContextMatch() && doDispatch(result)) return;
if (doDispatch(result)) return;
// There is no substitutions so show the context
@@ -260,7 +260,7 @@ public class GlobalMatchingVisitor extends AbstractMatchingVisitor {
}
else {
for (final PsiElement matchStatement : matchedNodes) {
result.addChild(new MatchResultImpl(MatchResult.LINE_MATCH, matchStatement.getText(), new SmartPsiPointer(matchStatement), true));
result.addChild(new MatchResultImpl(MatchResult.LINE_MATCH, matchStatement.getText(), new SmartPsiPointer(matchStatement), false));
}
result.setMatchRef(new SmartPsiPointer(match));
@@ -20,6 +20,7 @@ public final class MatchResultImpl extends MatchResult {
private boolean myScopeMatch;
private boolean myMultipleMatch;
private MatchResultImpl parent = null;
MatchResultImpl() {
}
@@ -143,9 +144,22 @@ public final class MatchResultImpl extends MatchResult {
}
public void addChild(MatchResult result) {
if (result instanceof MatchResultImpl) {
((MatchResultImpl)result).parent = this;
}
myChildren.add(result);
}
@Override
public MatchResult getRoot() {
if (parent == null) return this;
MatchResultImpl root = parent;
while (root.parent != null) {
root = root.parent;
}
return root;
}
public void setMatchImage(String matchImage) {
this.matchImage = matchImage;
}
@@ -30,7 +30,7 @@ class ReplacementInfoImpl implements ReplacementInfo {
private void init(Project project) {
fillPointerList(project);
fillVariableMap(matchResult);
fillVariableMap(matchResult.getRoot());
for(Map.Entry<String, MatchResult> entry : variableMap.entrySet()) {
fillElementToVariableNameMap(entry.getKey(), entry.getValue());
}
@@ -116,12 +116,10 @@ public class Replacer {
lastElement = parent.getLastChild();
}
matchOptions.setResultIsContextMatch(true);
CollectingMatchResultSink sink = new CollectingMatchResultSink();
matcher.testFindMatches(sink, matchOptions);
final List<ReplacementInfo> resultPtrList = new SmartList<>();
for (final MatchResult result : sink.getMatches()) {
resultPtrList.add(buildReplacement(result));
}
@@ -217,7 +215,7 @@ public class Replacer {
if (element==null || !element.isWritable() || !element.isValid()) return null;
final PsiElement elementParent = element.getParent();
final PsiElement elementParent = StructuralSearchUtil.getPresentableElement(element).getParent();
CodeStyleManager.getInstance(project).performActionWithFormatterDisabled(
(Runnable)() -> {
@@ -353,7 +351,7 @@ public class Replacer {
public ReplacementInfo buildReplacement(MatchResult result) {
final ReplacementInfoImpl replacementInfo = new ReplacementInfoImpl(result, project);
if (replacementBuilder==null) {
if (replacementBuilder == null) {
replacementBuilder = new ReplacementBuilder(project, options);
}
replacementInfo.setReplacement(replacementBuilder.process(result, replacementInfo, options.getMatchOptions().getFileType()));
@@ -95,7 +95,7 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
" JOptionPane.showMessageDialog(null, \"MESSAGE\");\n" +
" }\n" +
"}";
String s2 = "JOptionPane.'showDialog(null, '_msg);";
String s2 = "JOptionPane.'_showDialog(null, '_msg);";
String s3 = "//FIXME provide a parent frame\n" +
"JOptionPane.$showDialog$(null, $msg$);";
@@ -230,7 +230,7 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
assertEquals("Replacement of init in definition + empty substitution", expectedResult7, replacer.testReplace(str16,str17,str18,options));
String str19 = " aaa(bbb);";
String str20 = "'t('_);";
String str20 = "'_t('_);";
String str21 = "$t$(ccc);";
String expectedResult8 = " aaa(ccc);";
@@ -253,7 +253,7 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
" });\n" +
" }\n" +
" });";
String str26 = " LaterInvocator.invokeLater('Params{1,10});";
String str26 = " LaterInvocator.invokeLater('_Params{1,10});";
String str27 = " com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater($Params$);";
String expectedResult10 = " com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater(new Runnable() {\n" +
" public void run() {\n" +
@@ -272,7 +272,7 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
" processedElement.getTextOffset(), true,\n" +
" !myUsageViewDescriptor.toMarkInvalidOrReadonlyUsages(), null);";
String str29 = "new UTElementNode('_param, '_directory, '_null, '_0, '_true, !'_descr.toMarkInvalidOrReadonlyUsages(),\n" +
" 'referencesWord)";
" '_referencesWord)";
String str30 = "new UTElementNode($param$, $directory$, $null$, $0$, $true$, true,\n" +
" $referencesWord$)";
@@ -281,7 +281,7 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
assertEquals("Replace in def initializer", expectedResult11, replacer.testReplace(str28,str29,str30,options));
String s31 = "a = b; b = c; a=a; c=c;";
String s32 = "'a = 'a;";
String s32 = "'_a = '_a;";
String s33 = "1 = 1;";
String expectedResult12 = "a = b; b = c; 1 = 1; 1 = 1;";
@@ -300,7 +300,7 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
" ParamChecker.isTrue(2==2, \"!!!\");\n" +
"} catch(Exception ex) {}";
String s38 = "try {\n" +
" 'Statement{0,100};\n" +
" '_Statement{0,100};\n" +
"} catch(Exception ex) {}";
String s39 = "$Statement$;";
@@ -499,7 +499,7 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
String s4 = "if (true) System.out.println(\"1111\"); else System.out.println(\"2222\");\n" +
"while(true) System.out.println(\"1111\");";
String s5 = "System.out.println('Test);";
String s5 = "System.out.println('_Test);";
String s6 = "/* System.out.println($Test$); */";
String expectedResult2 = "if (true) /* System.out.println(\"1111\"); */; else /* System.out.println(\"2222\"); */;\n" +
"while(true) /* System.out.println(\"1111\"); */;";
@@ -676,10 +676,10 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
"}";
String s11 = "new Runnable() {\n" +
" public void run() {\n" +
" 'l{2,2};\n" +
" '_l{2,2};\n" +
" }\n" +
" public void run2() {\n" +
" 'l;\n" +
" '_l;\n" +
" }\n" +
"\n" +
" };";
@@ -726,7 +726,7 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
" abstract void f();\n" +
"}";
assertEquals("same multiple occurences 2 times", expectedResult4, replacer.testReplace(s10,s11,s12,options));
assertEquals("same multiple occurrences 2 times", expectedResult4, replacer.testReplace(s10,s11,s12,options));
options.setToReformatAccordingToStyle(formatAccordingToStyle);
@@ -754,7 +754,7 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
" }";
String s14 = " PsiLock.LOCK.acquire();\n" +
" try {\n" +
" 'T{1,1000};\n" +
" '_T{1,1000};\n" +
" }\n" +
" finally {\n" +
" PsiLock.LOCK.release();\n" +
@@ -814,7 +814,7 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
"}";
String s17 = "synchronized(lock) {\n" +
" 'Statement*;\n" +
" '_Statement*;\n" +
"}";
String s18 = "$Statement$;";
@@ -923,8 +923,8 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
" this.closeOutOnExit = closeOutOnExit;\n" +
"}\n" +
"}";
String s14 = "class 'Class extends Thread {\n" +
" 'Class('_ParameterType* '_ParameterName*) {\n" +
String s14 = "class '_Class extends Thread {\n" +
" '_Class('_ParameterType* '_ParameterName*) {\n" +
"\t super (CustomThreadGroup.getThreadGroup(), '_superarg* );\n" +
" '_Statement*;\n" +
" }\n" +
@@ -1305,7 +1305,7 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
public void testReplaceReturnWithArrayInitializer() {
String searchIn = "return ( new String[]{CoreVars.CMUAudioPort + \"\"} );";
String searchFor = "return ( 'A );";
String searchFor = "return ( '_A );";
String replaceBy = "return $A$;";
String expectedResult = "return new String[]{CoreVars.CMUAudioPort + \"\"};";
@@ -1625,7 +1625,7 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
public void testUseStaticImport() {
final String in = "class X {{ Math.abs(-1); }}";
final String what = "Math.abs('a)";
final String what = "Math.abs('_a)";
final String by = "Math.abs($a$)";
final boolean save = options.isToUseStaticImport();
options.setToUseStaticImport(true);
@@ -1647,7 +1647,7 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
" System.out.println(Collections.<String>emptyList());" +
" }" +
"}";
final String what3 = "'_q.'_method:[regex( println )]('a)";
final String what3 = "'_q.'_method:[regex( println )]('_a)";
final String by3 = "$q$.$method$($a$)";
final String expected4 = "import java.util.Collections;" +
"import static java.lang.System.out;" +
@@ -1899,7 +1899,7 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
" System.out.println(\"blah\");\n" +
"} finally {\n" +
"}\n";
String s2 = "try {\n" + " 'statement*;\n" + "} finally {\n" + " \n" + "}";
String s2 = "try {\n" + " '_statement*;\n" + "} finally {\n" + " \n" + "}";
String replacement = "$statement$;";
String expected = "String[] a = {\"a\"};\n" +
" System.out.println(\"blah\");\n";
@@ -2367,4 +2367,43 @@ public class StructuralReplaceTest extends StructuralReplaceTestCase {
"}",
replacer.testReplace(in, what, by, options, true));
}
public void testReplaceTarget() {
String in = "import org.junit.Test;" +
"class Help {" +
" private String s = \"hello\";" +
" @Test" +
" public void testThisThing(){" +
" System.out.println();" +
" System.out.println();" +
" System.out.println();" +
" s = null;" +
" }" +
"}";
String what = "class 'Class {" +
" '_FieldType '_FieldName;" +
" @'_Annotation" +
" '_MethodType '_MethodName() {" +
" '_Statement*;" +
" '_FieldName = null;" +
" }" +
"}";
String by = "class $Class$ {" +
" $FieldType$ $FieldName$;" +
" @$Annotation$" +
" $MethodType$ $MethodName$() {" +
" $Statement$;" +
" }" +
"}";
assertEquals("import org.junit.Test;" +
"class Help {" +
" private String s=\"hello\";" +
" @Test" +
" public void testThisThing() {" +
" System.out.println();" +
" System.out.println();" +
" System.out.println();" +
" }" +
"}", replacer.testReplace(in, what, by, options, true));
}
}
@@ -1,4 +1,4 @@
// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.structuralsearch;
import com.intellij.openapi.fileTypes.StdFileTypes;
@@ -39,7 +39,7 @@ public class XmlStructuralReplaceTest extends StructuralReplaceTestCase {
" <separator/>\n" +
" <reference id=\"ExternalToolsGroup\"/>\n" +
"</group>";
String s5 = "<reference id=\"'Value\"/>";
String s5 = "<reference id=\"'_Value\"/>";
String s6 = "<reference ref=\"$Value$\"/>";
actualResult = replacer.testReplace(s4,s5,s6,options);
@@ -60,7 +60,7 @@ public class XmlStructuralReplaceTest extends StructuralReplaceTestCase {
String s7 = "<h4 class=\"a\">My title<aaa>ZZZZ</aaa> My title 3</h4>\n" +
"<h4>My title 2</h4>";
String s8 = "<h4 class=\"a\">'Content*</h4>";
String s8 = "<h4 class=\"a\">'_Content*</h4>";
String s9 = "<h5>$Content$</h5>";
actualResult = replacer.testReplace(s7,s8,s9,options);
@@ -77,14 +77,14 @@ public class XmlStructuralReplaceTest extends StructuralReplaceTestCase {
"<h4>My title 2</h4>";
assertEquals("Delete tag", expectedResult, replacer.testReplace(s7, s8, "", options));
String what = "<'H:h4 class=\"a\">'_Content*</'H>";
String what = "<'_H:h4 class=\"a\">'_Content*</'_H>";
String by = "<$H$>$Content$</$H$>";
expectedResult = "<h4>My title <aaa>ZZZZ</aaa> My title 3</h4>\n" +
"<h4>My title 2</h4>";
assertEquals("Replace with variable", expectedResult, replacer.testReplace(s7, what, by, options));
String in = "<b>Cry 'Havoc!', and <i>let slip the<br> dogs of war</i></b>";
what = "<'Tag:b >'_Content2*</'Tag>";
what = "<'_Tag:b >'_Content2*</'_Tag>";
by = "<$Tag$ id=\"unique\">$Content2$</$Tag$>";
expectedResult = "<b id=\"unique\">Cry 'Havoc!', and <i>let slip the<br> dogs of war</i></b>";
assertEquals("Replace complex content with variable", expectedResult, replacer.testReplace(in, what, by, options));
@@ -104,7 +104,7 @@ public class XmlStructuralReplaceTest extends StructuralReplaceTestCase {
public void testHtmlAddAttribute() {
String in = "<input class=\"other\" type=\"text\" ng-model=\"someModel\" placeholder=\"Some placeholder\" />";
String what = "<input 'a* />";
String what = "<input '_a* />";
String by = "<input $a$ id=\"someId1\" />";
String expected = "<input class=\"other\" type=\"text\" ng-model=\"someModel\" placeholder=\"Some placeholder\" id=\"someId1\" />";