--- a/java.hints/src/org/netbeans/modules/java/hints/threading/Tiny.java +++ a/java.hints/src/org/netbeans/modules/java/hints/threading/Tiny.java @@ -243,11 +243,11 @@ } @Hint(displayName = "#DN_org.netbeans.modules.java.hints.threading.Tiny.synchronizedOnLock", description = "#DESC_org.netbeans.modules.java.hints.threading.Tiny.synchronizedOnLock", category="thread", suppressWarnings="SynchroniziationOnLockObject") - @TriggerPattern(value="synchronized ($lock) {$statements$;}", + @TriggerPattern(value="synchronized ($lock) #{s}#{$statements$;}", constraints=@ConstraintVariableType(variable="$lock", type="java.util.concurrent.locks.Lock")) public static ErrorDescription synchronizedOnLock(HintContext ctx) { String fixDisplayName = NbBundle.getMessage(Tiny.class, "FIX_SynchronizedOnLock"); - Fix f = JavaFixUtilities.rewriteFix(ctx, fixDisplayName, ctx.getPath(), "$lock.lock(); try {$statements$;} finally {$lock.unlock();}"); + Fix f = JavaFixUtilities.rewriteFix(ctx, fixDisplayName, ctx.getPath(), "$lock.lock(); try #{s}#{$statements$;} finally {$lock.unlock();}", ctx.getAnchors()); String displayName = NbBundle.getMessage(Tiny.class, "ERR_SynchronizedOnLock"); return ErrorDescriptionFactory.forName(ctx, ctx.getPath(), displayName, f); --- a/java.source/apichanges.xml +++ a/java.source/apichanges.xml @@ -108,6 +108,18 @@ + + + Places in search pattern can be named for further navigation in code transformation process + + + + + + The Pattern is able to accept and record places (TreePaths) that match named Trees + in the passed tree pattern. The recorded matches are available from the {@code Occurrence} result class. + + Added a possibility to specify remote javadoc handling policy --- a/java.source/nbproject/project.properties +++ a/java.source/nbproject/project.properties @@ -46,7 +46,7 @@ javadoc.title=Java Source javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml -spec.version.base=0.140.0 +spec.version.base=0.141.0 test.qa-functional.cp.extra=${refactoring.java.dir}/modules/ext/nb-javac-api.jar test.unit.run.cp.extra=${o.n.core.dir}/core/core.jar:\ ${o.n.core.dir}/lib/boot.jar:\ --- a/java.source/src/org/netbeans/api/java/source/matching/Matcher.java +++ a/java.source/src/org/netbeans/api/java/source/matching/Matcher.java @@ -42,11 +42,13 @@ package org.netbeans.api.java.source.matching; import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -175,7 +177,7 @@ State preinitializeState; if (variables != null) { - preinitializeState = State.from(variables, multiVariables, variables2Names); + preinitializeState = State.from(variables, multiVariables, variables2Names, new HashMap()); } else { preinitializeState = null; } @@ -186,8 +188,9 @@ } }; - for (Entry e : CopyFinder.internalComputeDuplicates(info, pattern.pattern, root, preinitializeState, pattern.remappable, cancel, pattern.variable2Type, opts.toArray(new Options[opts.size()])).entrySet()) { - result.add(new Occurrence(e.getKey(), e.getValue().variables, e.getValue().multiVariables, e.getValue().variables2Names, e.getValue().variablesRemapToElement, e.getValue().variablesRemapToTrees)); + for (Entry e : CopyFinder.internalComputeDuplicates(info, pattern.pattern, root, preinitializeState, pattern.remappable, cancel, pattern.variable2Type, pattern.anchorNames, opts.toArray(new Options[opts.size()])).entrySet()) { + result.add(new Occurrence(e.getKey(), e.getValue().variables, e.getValue().multiVariables, e.getValue().variables2Names, e.getValue().variablesRemapToElement, e.getValue().variablesRemapToTrees, + e.getValue().anchors)); } return Collections.unmodifiableCollection(result); --- a/java.source/src/org/netbeans/api/java/source/matching/Occurrence.java +++ a/java.source/src/org/netbeans/api/java/source/matching/Occurrence.java @@ -57,14 +57,17 @@ private final Map variables2Names; private final Map variablesRemapToElement; private final Map variablesRemapToTrees; - - Occurrence(TreePath occurrenceRoot, Map variables, Map> multiVariables, Map variables2Names, Map variablesRemapToElement, Map variablesRemapToTrees) { + private final Map anchors; + + Occurrence(TreePath occurrenceRoot, Map variables, Map> multiVariables, Map variables2Names, Map variablesRemapToElement, + Map variablesRemapToTrees, Map anchors) { this.occurrenceRoot = occurrenceRoot; this.variables = variables; this.multiVariables = multiVariables; this.variables2Names = variables2Names; this.variablesRemapToElement = variablesRemapToElement; this.variablesRemapToTrees = variablesRemapToTrees; + this.anchors = anchors; } /**The tree node that represents the occurrence. For multi-part patterns {@link Pattern#createSimplePattern(java.lang.Iterable) }, @@ -125,4 +128,13 @@ return variablesRemapToTrees; } + /** + * Retrieves trees matched to the named anchors in the pattern. + * For each matched anchor provides a TreePath to the corresponding Tree. + * @return matched named trees. + * @since 0.141 + */ + public Map getAnchors() { + return anchors; + } } --- a/java.source/src/org/netbeans/api/java/source/matching/Pattern.java +++ a/java.source/src/org/netbeans/api/java/source/matching/Pattern.java @@ -42,6 +42,7 @@ package org.netbeans.api.java.source.matching; import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; import java.util.ArrayList; import java.util.Arrays; @@ -64,6 +65,7 @@ final Map variable2Type; final Collection remappable; final boolean allowRemapToTrees; + Map anchorNames = Collections.emptyMap(); /**Creates a simple pattern. Tree nodes that are semantically equivalent * to the given pattern will match it. @@ -154,4 +156,18 @@ this.allowRemapToTrees = allowRemapToTrees; } + /** + * Provides naming for certain trees in the pattern. The names are then assigned by matched real trees, so the caller may + * inspect details of the matched code, without the need to repeat some pattern-matching fuzzy logic to dive into the + * matched subtree. + * + * @param anchorNames Names for individual Trees. Trees that do not exist somewhere in the pattern will be ignored + * and no match will be assigned to them + * @return this instance + * @since 0.141 + */ + public final Pattern setAnchors(Map anchorNames) { + this.anchorNames = anchorNames == null ? Collections.emptyMap() : anchorNames; + return this; + } } --- a/java.source/src/org/netbeans/modules/java/source/matching/CopyFinder.java +++ a/java.source/src/org/netbeans/modules/java/source/matching/CopyFinder.java @@ -131,8 +131,9 @@ private final Cancel cancel; private static final String CLASS = "class"; //NOI18N private final Set options; - + private Map anchorNames; private Map designedTypeHack; + private Map anchors = new HashMap<>(); private static Set options(Options... options) { Set result = EnumSet.noneOf(Options.class); @@ -152,8 +153,14 @@ this.cancel = cancel; this.options = options; } + + public static Map internalComputeDuplicates(CompilationInfo info, Collection searchingFor, TreePath scope, State preinitializedState, Collection variablesWithAllowedRemap, Cancel cancel, Map designedTypeHack, Options... options) { + return internalComputeDuplicates(info, searchingFor, scope, preinitializedState, variablesWithAllowedRemap, cancel, designedTypeHack, + new HashMap(), options); + } - public static Map internalComputeDuplicates(CompilationInfo info, Collection searchingFor, TreePath scope, State preinitializedState, Collection variablesWithAllowedRemap, Cancel cancel, Map designedTypeHack, Options... options) { + public static Map internalComputeDuplicates(CompilationInfo info, Collection searchingFor, TreePath scope, State preinitializedState, Collection variablesWithAllowedRemap, Cancel cancel, Map designedTypeHack, + Map anchorNames, Options... options) { TreePath first = searchingFor.iterator().next(); Set optionsSet = EnumSet.noneOf(Options.class); @@ -170,7 +177,7 @@ f.variablesWithAllowedRemap = variablesWithAllowedRemap != null ? new HashSet(variablesWithAllowedRemap) : Collections.emptySet(); f.allowVariablesRemap = variablesWithAllowedRemap != null; f.nocheckOnAllowVariablesRemap = variablesWithAllowedRemap != null; - + f.anchorNames = anchorNames; if (preinitializedState != null) { f.bindState = State.copyOf(f.preinitializeState = preinitializedState); } @@ -215,7 +222,7 @@ Map variables2Names = new HashMap(e.getValue().variables2Names); Map remapElements = new HashMap(e.getValue().variablesRemapToElement); Map remapTrees = new HashMap(e.getValue().variablesRemapToTrees); - + Map anchors = new HashMap<>(e.getValue().anchors); toProcess.next(); while (toProcess.hasNext()) { @@ -229,7 +236,7 @@ ver.variablesWithAllowedRemap = variablesWithAllowedRemap != null ? new HashSet(variablesWithAllowedRemap) : Collections.emptySet(); ver.allowVariablesRemap = variablesWithAllowedRemap != null; ver.nocheckOnAllowVariablesRemap = variablesWithAllowedRemap != null; - ver.bindState = State.from(variables, multiVariables, variables2Names); + ver.bindState = State.from(variables, multiVariables, variables2Names, anchors); if (ver.allowVariablesRemap) { ver.bindState = State.from(ver.bindState, remapElements, remapTrees); @@ -244,9 +251,10 @@ variables2Names = ver.bindState.variables2Names; remapElements = ver.bindState.variablesRemapToElement; remapTrees = ver.bindState.variablesRemapToTrees; + anchors = ver.bindState.anchors; } - result.put(e.getKey(), new VariableAssignments(variables, multiVariables, variables2Names, remapElements, remapTrees)); + result.put(e.getKey(), new VariableAssignments(variables, multiVariables, variables2Names, remapElements, remapTrees, anchors)); } return result; @@ -323,6 +331,17 @@ @Override public Boolean scan(Tree node, TreePath p) { + Boolean r = scan0(node, p); + if (r != null && r && anchorNames != null) { + String tn = anchorNames.get(p.getLeaf()); + if (tn != null) { + bindState.anchors.put(tn, getCurrentPath()); + } + } + return r; + } + + Boolean scan0(Tree node, TreePath p) { if (cancel.isCancelled()) { return false; } @@ -1788,21 +1807,23 @@ public final Map variables2Names; public final Map variablesRemapToElement; public final Map variablesRemapToTrees; + public final Map anchors; - public VariableAssignments(Map variables, Map> multiVariables, Map variables2Names) { - this.variables = variables; - this.multiVariables = multiVariables; - this.variables2Names = variables2Names; - this.variablesRemapToElement = null; - this.variablesRemapToTrees = null; - } - - public VariableAssignments(Map variables, Map> multiVariables, Map variables2Names, Map variablesRemapToElement, Map variablesRemapToTrees) { +// public VariableAssignments(Map variables, Map> multiVariables, Map variables2Names) { +// this.variables = variables; +// this.multiVariables = multiVariables; +// this.variables2Names = variables2Names; +// this.variablesRemapToElement = null; +// this.variablesRemapToTrees = null; +// } +// + public VariableAssignments(Map variables, Map> multiVariables, Map variables2Names, Map variablesRemapToElement, Map variablesRemapToTrees, Map anchors) { this.variables = variables; this.multiVariables = multiVariables; this.variables2Names = variables2Names; this.variablesRemapToElement = variablesRemapToElement; this.variablesRemapToTrees = variablesRemapToTrees; + this.anchors = anchors; } VariableAssignments(State state) { @@ -1811,6 +1832,7 @@ this.variables2Names = state.variables2Names; this.variablesRemapToElement = state.variablesRemapToElement; this.variablesRemapToTrees = state.variablesRemapToTrees; + this.anchors = state.anchors; } } @@ -1835,28 +1857,35 @@ final Map variables2Names; final Map variablesRemapToElement; final Map variablesRemapToTrees; + Map anchors; - private State(Map variables, Map> multiVariables, Map variables2Names, Map variablesRemapToElement, Map variablesRemapToTrees) { + private State(Map variables, Map> multiVariables, Map variables2Names, Map variablesRemapToElement, + Map variablesRemapToTrees, Map anchors) { this.variables = variables; this.multiVariables = multiVariables; this.variables2Names = variables2Names; this.variablesRemapToElement = variablesRemapToElement; this.variablesRemapToTrees = variablesRemapToTrees; + this.anchors = anchors; } public static State empty() { - return new State(new HashMap(), new HashMap>(), new HashMap(), new HashMap(), new HashMap()); + return new State(new HashMap(), new HashMap>(), new HashMap(), new HashMap(), new HashMap(), + new HashMap()); } public static State copyOf(State original) { - return new State(new HashMap(original.variables), new HashMap>(original.multiVariables), new HashMap(original.variables2Names), new HashMap(original.variablesRemapToElement), new HashMap(original.variablesRemapToTrees)); + return new State(new HashMap(original.variables), new HashMap>(original.multiVariables), new HashMap(original.variables2Names), new HashMap(original.variablesRemapToElement), + new HashMap(original.variablesRemapToTrees), new HashMap<>(original.anchors)); } public static State from(State original, Map variablesRemapToElement, Map variablesRemapToTrees) { - return new State(new HashMap(original.variables), new HashMap>(original.multiVariables), new HashMap(original.variables2Names), variablesRemapToElement, variablesRemapToTrees); + return new State(new HashMap(original.variables), new HashMap>(original.multiVariables), new HashMap(original.variables2Names), variablesRemapToElement, variablesRemapToTrees, + new HashMap(original.anchors)); } - public static State from(Map variables, Map> multiVariables, Map variables2Names) { - return new State(variables, multiVariables, variables2Names, new HashMap(), new HashMap()); + public static State from(Map variables, Map> multiVariables, Map variables2Names, + Map anchors) { + return new State(variables, multiVariables, variables2Names, new HashMap(), new HashMap(), anchors); } } --- a/spi.java.hints/apichanges.xml +++ a/spi.java.hints/apichanges.xml @@ -46,6 +46,22 @@ Java Hints SPI + + + Added ability to associate parts of template-based fix with pieces of original source + + + + + +

+ HintContext now provides named places specified in the trigger pattern and the respective matched + TreePaths in the original text. JavaFix is able to use that information to associate same-named places + in the fix template to the original code pieces to preserve comments/formatting. +

+
+
+
Added ability to specify sort text for JavaFix --- a/spi.java.hints/nbproject/project.properties +++ a/spi.java.hints/nbproject/project.properties @@ -1,7 +1,7 @@ is.autoload=true javac.source=1.7 javac.compilerargs=-Xlint -Xlint:-serial -spec.version.base=1.22.0 +spec.version.base=1.23.0 requires.nb.javac=true javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml --- a/spi.java.hints/src/org/netbeans/modules/java/hints/spiimpl/JavaFixImpl.java +++ a/spi.java.hints/src/org/netbeans/modules/java/hints/spiimpl/JavaFixImpl.java @@ -152,7 +152,10 @@ public abstract ChangeInfo process(JavaFix jf, WorkingCopy wc, boolean canShowUI, Map resourceContent, Collection fileChanges) throws Exception; public abstract FileObject getFile(JavaFix jf); public abstract Map getOptions(JavaFix jf); - public abstract Fix rewriteFix(CompilationInfo info, String displayName, TreePath what, final String to, Map parameters, Map> parametersMulti, final Map parameterNames, Map constraints, Map options, String... imports); + public abstract Fix rewriteFix(CompilationInfo info, String displayName, TreePath what, final String to, Map parameters, Map> parametersMulti, final Map parameterNames, Map constraints, Map options, Map anchors, String... imports); + public final Fix rewriteFix(CompilationInfo info, String displayName, TreePath what, final String to, Map parameters, Map> parametersMulti, final Map parameterNames, Map constraints, Map options, String... imports) { + return rewriteFix(info, displayName, what, to, parameters, parametersMulti, parameterNames, constraints, options, new HashMap(), imports); + } public abstract Fix createSuppressWarningsFix(CompilationInfo compilationInfo, TreePath treePath, String... keys); public abstract List createSuppressWarnings(CompilationInfo compilationInfo, TreePath treePath, String... keys); public abstract List resolveDefaultFixes(HintContext ctx, Fix... provided); --- a/spi.java.hints/src/org/netbeans/modules/java/hints/spiimpl/SPIAccessor.java +++ a/spi.java.hints/src/org/netbeans/modules/java/hints/spiimpl/SPIAccessor.java @@ -77,7 +77,7 @@ accessor = instance; } - public abstract HintContext createHintContext(CompilationInfo info, HintsSettings settings, HintMetadata metadata, TreePath path, Map variables, Map> multiVariables, Map variableNames, Map constraints, Collection problems, boolean bulkMode, AtomicBoolean cancel, int caret); + public abstract HintContext createHintContext(CompilationInfo info, HintsSettings settings, HintMetadata metadata, TreePath path, Map variables, Map> multiVariables, Map variableNames, Map constraints, Collection problems, boolean bulkMode, AtomicBoolean cancel, int caret, Map anchors); public abstract HintContext createHintContext(CompilationInfo info, HintsSettings settings, HintMetadata metadata, TreePath path, Map variables, Map> multiVariables, Map variableNames); public abstract HintMetadata getHintMetadata(HintContext ctx); public abstract HintsSettings getHintSettings(HintContext ctx); --- a/spi.java.hints/src/org/netbeans/modules/java/hints/spiimpl/Utilities.java +++ a/spi.java.hints/src/org/netbeans/modules/java/hints/spiimpl/Utilities.java @@ -123,6 +123,8 @@ import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.AnnotationValueVisitor; @@ -329,12 +331,23 @@ return parseAndAttribute(info, pattern, scope, null); } + public static Tree parseAndAttributeNamed(CompilationInfo info, String pattern, Scope scope, Map namedTrees) { + return parseAndAttribute(info, JavaSourceAccessor.getINSTANCE().getJavacTask(info), pattern, scope, new SourcePositions[1], null, namedTrees); + } + public static Tree parseAndAttribute(CompilationInfo info, String pattern, Scope scope, Collection> errors) { return parseAndAttribute(info, JavaSourceAccessor.getINSTANCE().getJavacTask(info), pattern, scope, errors); } + public static Tree parseAndAttributeNamed(CompilationInfo info, String pattern, Scope scope, Collection> errors, Map namedTrees) { + return parseAndAttribute(info, + JavaSourceAccessor.getINSTANCE().getJavacTask(info), pattern, + scope, new SourcePositions[1], errors, namedTrees); + } + public static Tree parseAndAttribute(CompilationInfo info, String pattern, Scope scope, SourcePositions[] sourcePositions, Collection> errors) { - return parseAndAttribute(info, JavaSourceAccessor.getINSTANCE().getJavacTask(info), pattern, scope, sourcePositions, errors); + return parseAndAttribute(info, JavaSourceAccessor.getINSTANCE().getJavacTask(info), pattern, scope, sourcePositions, errors, + new HashMap()); } public static Tree parseAndAttribute(JavacTaskImpl jti, String pattern) { @@ -346,26 +359,30 @@ } public static Tree parseAndAttribute(JavacTaskImpl jti, String pattern, SourcePositions[] sourcePositions, Collection> errors) { - return parseAndAttribute(null, jti, pattern, null, sourcePositions, errors); + return parseAndAttribute(null, jti, pattern, null, sourcePositions, errors, new HashMap()); } private static Tree parseAndAttribute(CompilationInfo info, JavacTaskImpl jti, String pattern, Scope scope, Collection> errors) { - return parseAndAttribute(info, jti, pattern, scope, new SourcePositions[1], errors); + return parseAndAttribute(info, jti, pattern, scope, new SourcePositions[1], errors, new HashMap()); } - private static Tree parseAndAttribute(CompilationInfo info, JavacTaskImpl jti, String pattern, Scope scope, SourcePositions[] sourcePositions, Collection> errors) { + @SuppressWarnings("StringEquality") + private static Tree parseAndAttribute(CompilationInfo info, JavacTaskImpl jti, String pattern, Scope scope, final SourcePositions[] sourcePositions, Collection> errors, + final Map namedTrees) { Context c = jti.getContext(); TreeFactory make = TreeFactory.instance(c); List> patternTreeErrors = new LinkedList>(); Tree toAttribute; - Tree patternTree = toAttribute = !isStatement(pattern) ? parseExpression(c, pattern, true, sourcePositions, patternTreeErrors) : null; + final Map pos = new HashMap<>(); + String pattern2 = removeAnchors(pattern, pos); + + Tree patternTree = toAttribute = !isStatement(pattern2) ? parseExpression(c, pattern2, true, sourcePositions, patternTreeErrors) : null; int offset = 0; boolean expression = true; boolean classMember = false; - - if (pattern.startsWith("case ")) {//XXX: should be a lexer token + if (pattern2.startsWith("case ")) {//XXX: should be a lexer token List> currentPatternTreeErrors = new LinkedList>(); - Tree switchTree = parseStatement(c, "switch ($$foo) {" + pattern + "}", sourcePositions, currentPatternTreeErrors); + Tree switchTree = parseStatement(c, "switch ($$foo) {" + pattern2 + "}", sourcePositions, currentPatternTreeErrors); offset = "switch ($$foo) {".length(); patternTreeErrors = currentPatternTreeErrors; @@ -376,7 +393,7 @@ if (patternTree == null || isErrorTree(patternTree)) { SourcePositions[] currentPatternTreePositions = new SourcePositions[1]; List> currentPatternTreeErrors = new LinkedList>(); - Tree currentPatternTree = parseStatement(c, "{" + pattern + "}", currentPatternTreePositions, currentPatternTreeErrors); + Tree currentPatternTree = parseStatement(c, "{" + pattern2 + "}", currentPatternTreePositions, currentPatternTreeErrors); assert currentPatternTree.getKind() == Kind.BLOCK : currentPatternTree.getKind(); @@ -402,7 +419,7 @@ //maybe a class member? SourcePositions[] classPatternTreePositions = new SourcePositions[1]; List> classPatternTreeErrors = new LinkedList>(); - Tree classPatternTree = parseExpression(c, "new Object() {" + pattern + "}", false, classPatternTreePositions, classPatternTreeErrors); + Tree classPatternTree = parseExpression(c, "new Object() {" + pattern2 + "}", false, classPatternTreePositions, classPatternTreeErrors); if (!containsError(classPatternTree)) { sourcePositions[0] = classPatternTreePositions[0]; @@ -442,7 +459,7 @@ if (Utilities.isPureMemberSelect(patternTree, false)) { SourcePositions[] varPositions = new SourcePositions[1]; List> varErrors = new LinkedList>(); - Tree var = parseExpression(c, pattern + ".Class.class;", false, varPositions, varErrors); + Tree var = parseExpression(c, pattern2 + ".Class.class;", false, varPositions, varErrors); attributeTree(jti, var, scope, varErrors); @@ -537,9 +554,50 @@ } sourcePositions[0] = new OffsetSourcePositions(sourcePositions[0], -offset); - + // if the caller wants to collect named trees AND there have been some named + // trees, collect their positions. + if (pattern2 != pattern && !pos.isEmpty()) { + TreeScanner scn = new TreeScanner() { + + @Override + public Object scan(Tree node, Object p) { + if (node != null) { + int x = (int)sourcePositions[0].getStartPosition(null, node); + if (x > 0) { + String n = pos.get(x); + if (n != null) { + namedTrees.put(node, n); + } + } + } + return super.scan(node, p); //To change body of generated methods, choose Tools | Templates. + } + + }; + + scn.scan(patternTree, null); + } return patternTree; } + + private static final Pattern REGEXP_ANCHOR = Pattern.compile("#\\{(\\p{Alnum}+)\\}#"); // NOI18N + + private static String removeAnchors(String pattern, Map positions) { + Matcher m = REGEXP_ANCHOR.matcher(pattern); + StringBuilder sb = null; + int last = 0; + while (m.find()) { + int s = m.start(); + int e = m.end(); + if (sb == null) { + sb = new StringBuilder(pattern.length()); + } + sb.append(pattern.subSequence(last, s)); + positions.put(sb.length(), m.group(1)); + last = e; + } + return sb == null ? pattern : sb.append(pattern.substring(last)).toString(); + } static boolean isError(Element el) { return (el == null || (el.getKind() == ElementKind.CLASS) && isError(((TypeElement) el).asType())); --- a/spi.java.hints/src/org/netbeans/modules/java/hints/spiimpl/hints/HintsInvoker.java +++ a/spi.java.hints/src/org/netbeans/modules/java/hints/spiimpl/hints/HintsInvoker.java @@ -542,7 +542,6 @@ constraints.put(e.getKey(), designedType); } - Pattern pattern = PatternCompiler.compile(info, occ.getKey(), constraints, d.getImports()); for (TreePath candidate : occ.getValue()) { @@ -559,7 +558,7 @@ for (HintDescription hd : patternHints.get(d)) { HintMetadata hm = hd.getMetadata(); - HintContext c = SPIAccessor.getINSTANCE().createHintContext(info, settings, hm, candidate, verifiedVariables.getVariables(), verifiedVariables.getMultiVariables(), verifiedVariables.getVariables2Names(), constraints, problems, bulkMode, cancel, caret); + HintContext c = SPIAccessor.getINSTANCE().createHintContext(info, settings, hm, candidate, verifiedVariables.getVariables(), verifiedVariables.getMultiVariables(), verifiedVariables.getVariables2Names(), constraints, problems, bulkMode, cancel, caret, verifiedVariables.getAnchors()); if (!Collections.disjoint(suppressedWarnings, hm.suppressWarnings)) continue; @@ -653,7 +652,7 @@ } } - HintContext c = SPIAccessor.getINSTANCE().createHintContext(info, settings, hm, path, Collections.emptyMap(), Collections.>emptyMap(), Collections.emptyMap(), Collections.emptyMap(), new ArrayList(), bulkMode, cancel, caret); + HintContext c = SPIAccessor.getINSTANCE().createHintContext(info, settings, hm, path, Collections.emptyMap(), Collections.>emptyMap(), Collections.emptyMap(), Collections.emptyMap(), new ArrayList(), bulkMode, cancel, caret, null); Collection errors = runHint(hd, c); if (errors != null) { --- a/spi.java.hints/src/org/netbeans/modules/java/hints/spiimpl/pm/PatternCompiler.java +++ a/spi.java.hints/src/org/netbeans/modules/java/hints/spiimpl/pm/PatternCompiler.java @@ -45,6 +45,7 @@ import com.sun.source.tree.Scope; import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; +import java.util.HashMap; import java.util.Map; import javax.lang.model.type.TypeMirror; import org.netbeans.api.java.source.CompilationInfo; @@ -64,9 +65,10 @@ return null; //TODO: can happen? } - Tree patternTree = Utilities.parseAndAttribute(info, pattern, scope); + Map namedTrees = new HashMap<>(); + Tree patternTree = Utilities.parseAndAttributeNamed(info, pattern, scope, namedTrees); - return Pattern.createPatternWithFreeVariables(new TreePath(new TreePath(info.getCompilationUnit()), patternTree), constraints); + return Pattern.createPatternWithFreeVariables(new TreePath(new TreePath(info.getCompilationUnit()), patternTree), constraints).setAnchors(namedTrees); } } --- a/spi.java.hints/src/org/netbeans/spi/java/hints/HintContext.java +++ a/spi.java.hints/src/org/netbeans/spi/java/hints/HintContext.java @@ -51,6 +51,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.prefs.Preferences; import javax.lang.model.type.TypeMirror; +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.java.source.CompilationInfo; import org.netbeans.modules.java.hints.providers.spi.HintMetadata; import org.netbeans.modules.java.hints.spiimpl.MessageImpl; @@ -79,6 +81,7 @@ private final boolean bulkMode; private final AtomicBoolean cancel; private final int caret; + private Map anchors; private HintContext(CompilationInfo info, HintsSettings settings, HintMetadata metadata, TreePath path, Map variables, Map> multiVariables, Map variableNames, Map constraints, Collection problems, boolean bulkMode, AtomicBoolean cancel, int caret) { this.info = info; @@ -100,6 +103,11 @@ this.cancel = cancel; this.caret = caret; } + + HintContext setAnchors(Map anchors) { + this.anchors = anchors; + return this; + } public CompilationInfo getInfo() { return info; @@ -178,10 +186,31 @@ WARNING, ERROR; } + /** + * Provides a path to tree that matched a named position in the pattern. + * @param an anchor name + * @return TreePath that matched the node at the given position, or {@code null} + * @since 1.23 + */ + public final @CheckForNull TreePath getAnchoredPath(String an) { + return anchors == null ? null : anchors.get(an); + } + + /** + * Provides all anchors and their matched nodes. + * Only found anchors are present; those not matched have no entry in the returned + * Map + * @return anchor names associated with path to the matched Tree node + * @since 1.23 + */ + public final @NonNull Map getAnchors() { + return anchors == null ? Collections.emptyMap() : anchors; + } + static { SPIAccessor.setINSTANCE(new SPIAccessor() { - @Override public HintContext createHintContext(CompilationInfo info, HintsSettings settings, HintMetadata metadata, TreePath path, Map variables, Map> multiVariables, Map variableNames, Map constraints, Collection problems, boolean bulkMode, AtomicBoolean cancel, int caret) { - return new HintContext(info, settings, metadata, path, variables, multiVariables, variableNames, constraints, problems, bulkMode, cancel, caret); + @Override public HintContext createHintContext(CompilationInfo info, HintsSettings settings, HintMetadata metadata, TreePath path, Map variables, Map> multiVariables, Map variableNames, Map constraints, Collection problems, boolean bulkMode, AtomicBoolean cancel, int caret, Map anchors) { + return new HintContext(info, settings, metadata, path, variables, multiVariables, variableNames, constraints, problems, bulkMode, cancel, caret).setAnchors(anchors); } @Override public HintContext createHintContext(CompilationInfo info, HintsSettings settings, HintMetadata metadata, TreePath path, Map variables, Map> multiVariables, Map variableNames) { return new HintContext(info, settings, metadata, path, variables, multiVariables, variableNames, Collections.emptyMap(), new LinkedList(), false, new AtomicBoolean(), -1); --- a/spi.java.hints/src/org/netbeans/spi/java/hints/JavaFix.java +++ a/spi.java.hints/src/org/netbeans/spi/java/hints/JavaFix.java @@ -42,6 +42,7 @@ package org.netbeans.spi.java.hints; +import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -205,8 +206,10 @@ } @Override - public Fix rewriteFix(CompilationInfo info, String displayName, TreePath what, String to, Map parameters, Map> parametersMulti, Map parameterNames, Map constraints, Map options, String... imports) { - return JavaFixUtilities.rewriteFix(info, displayName, what, to, parameters, parametersMulti, parameterNames, constraints, options, imports); + public Fix rewriteFix(CompilationInfo info, String displayName, TreePath what, String to, Map parameters, Map> parametersMulti, Map parameterNames, Map constraints, Map options, + Map anchors, String... imports) { + return JavaFixUtilities.rewriteFix(info, displayName, what, to, parameters, parametersMulti, parameterNames, constraints, options, + anchors, imports); } @Override --- a/spi.java.hints/src/org/netbeans/spi/java/hints/JavaFixUtilities.java +++ a/spi.java.hints/src/org/netbeans/spi/java/hints/JavaFixUtilities.java @@ -159,10 +159,40 @@ * @return an editor fix that performs the required transformation */ public static Fix rewriteFix(HintContext ctx, String displayName, TreePath what, final String to) { - return rewriteFix(ctx.getInfo(), displayName, what, to, ctx.getVariables(), ctx.getMultiVariables(), ctx.getVariableNames(), ctx.getConstraints(), Collections.emptyMap()); + return rewriteFix( + ctx.getInfo(), displayName, what, to, ctx.getVariables(), + ctx.getMultiVariables(), ctx.getVariableNames(), ctx.getConstraints(), + Collections.emptyMap(), ctx.getAnchors()); + } + + /**Prepare a fix that will replace the given tree node ({@code what}) with the + * given code. Any variables in the {@code to} pattern will be replaced with their + * values from {@link HintContext#getVariables() }, {@link HintContext#getMultiVariables() } + * and {@link HintContext#getVariableNames() }. + *

+ * Unlike the other method, the {@code to} pattern may contain named tags, which will associate + * former code in the matched original to the code newly generated from the {@code to} pattern. + * Such generated code will transfer comments, javadoc and possibly even formatting from the original + * trees. The syntax for a named tag is {@code #{tagName}#} and will be associated to a Tree which + * starts exactly on the position immediately following the tag spec (so don't put any whitespace between }# and + * java code). + * @param ctx basic context for which the fix should be created + * @param displayName the display name of the fix + * @param what the tree node that should be replaced + * @param to the new code that should replaced the {@code what} tree node + * @param namedTrees trees in the original soruce that matched named tags. + * @return an editor fix that performs the required transformation + * @since 1.23 + */ + public static Fix rewriteFix(HintContext ctx, String displayName, TreePath what, final String to, Map namedTrees) { + return rewriteFix( + ctx.getInfo(), displayName, what, to, ctx.getVariables(), + ctx.getMultiVariables(), ctx.getVariableNames(), ctx.getConstraints(), + Collections.emptyMap(), namedTrees); } - static Fix rewriteFix(CompilationInfo info, String displayName, TreePath what, final String to, Map parameters, Map> parametersMulti, final Map parameterNames, Map constraints, Map options, String... imports) { + static Fix rewriteFix(CompilationInfo info, String displayName, TreePath what, final String to, Map parameters, Map> parametersMulti, final Map parameterNames, Map constraints, Map options, + Map namedTrees, String... imports) { final Map params = new HashMap(); final Map extraParamsData = new HashMap(); @@ -198,8 +228,19 @@ if (displayName == null) { displayName = defaultFixDisplayName(info, parameters, to); } + + Map namedHandles; + if (namedTrees != null && !namedTrees.isEmpty()) { + namedHandles = new HashMap<>(namedTrees.size()); + for (Map.Entry e : namedTrees.entrySet()) { + namedHandles.put(e.getKey(), TreePathHandle.create(e.getValue(), info)); + } + } else { + namedHandles = null; + } - return new JavaFixRealImpl(info, what, options, displayName, to, params, extraParamsData, paramsMulti, parameterNames, constraintsHandles, Arrays.asList(imports)).toEditorFix(); + return new JavaFixRealImpl(info, what, options, displayName, to, params, extraParamsData, paramsMulti, parameterNames, constraintsHandles, Arrays.asList(imports), + namedHandles).toEditorFix(); } /**Creates a fix that removes the given code corresponding to the given tree @@ -378,8 +419,10 @@ private final Map> constraintsHandles; private final Iterable imports; private final String to; + private final Map namedTrees; - public JavaFixRealImpl(CompilationInfo info, TreePath what, Map options, String displayName, String to, Map params, Map extraParamsData, Map> paramsMulti, final Map parameterNames, Map> constraintsHandles, Iterable imports) { + public JavaFixRealImpl(CompilationInfo info, TreePath what, Map options, String displayName, String to, Map params, Map extraParamsData, Map> paramsMulti, final Map parameterNames, Map> constraintsHandles, Iterable imports, + Map namedTrees) { super(info, what, options); this.displayName = displayName; @@ -390,6 +433,7 @@ this.parameterNames = parameterNames; this.constraintsHandles = constraintsHandles; this.imports = imports; + this.namedTrees = namedTrees; } @Override @@ -439,12 +483,28 @@ for (Entry> c : constraintsHandles.entrySet()) { constraints.put(c.getKey(), c.getValue().resolve(wc)); } + + Map origNamedTrees; + + if (namedTrees != null) { + origNamedTrees = new HashMap<>(namedTrees.size()); + for (Map.Entry e : namedTrees.entrySet()) { + TreePath x = e.getValue().resolve(wc); + if (x == null) { + // TODO - shouldn't we bail out ? A matched subtree no longer exists + continue; + } + origNamedTrees.put(e.getKey(), x); + } + } else { + origNamedTrees = Collections.emptyMap(); + } Scope scope = Utilities.constructScope(wc, constraints, imports); assert scope != null; - - Tree parsed = Utilities.parseAndAttribute(wc, to, scope); + Map namedTrees = new HashMap<>(); + Tree parsed = Utilities.parseAndAttributeNamed(wc, to, scope, namedTrees); if (parsed.getKind() == Kind.EXPRESSION_STATEMENT && ExpressionTree.class.isAssignableFrom(tp.getLeaf().getKind().asInterface())) { parsed = ((ExpressionStatementTree) parsed).getExpression(); @@ -564,6 +624,13 @@ // gen.copyComments(from, to, false); wc.rewrite(from, to); } + for (Map.Entry ne : namedTrees.entrySet()) { + TreePath origP = origNamedTrees.get(ne.getValue()); + if (origP == null) { + continue; + } + wc.getTreeMaker().asReplacementOf(ne.getKey(), origP.getLeaf(), true); + } } } --- a/spi.java.hints/test/unit/src/org/netbeans/spi/java/hints/JavaFixUtilitiesTest.java +++ a/spi.java.hints/test/unit/src/org/netbeans/spi/java/hints/JavaFixUtilitiesTest.java @@ -251,7 +251,11 @@ VariableTree variable = (VariableTree) clazz.getMembers().get(1); ExpressionTree init = variable.getInitializer(); TreePath tp = new TreePath(new TreePath(new TreePath(new TreePath(info.getCompilationUnit()), clazz), variable), init); - Fix fix = JavaFixUtilities.rewriteFix(info, "A", tp, orig, Collections.emptyMap(), Collections.>emptyMap(), Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap()); + Fix fix = JavaFixUtilities.rewriteFix(info, "A", tp, orig, Collections.emptyMap(), + Collections.>emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), Collections.emptyMap()); fix.implement(); String golden = replace(nue);