You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@netbeans.apache.org by ne...@apache.org on 2020/07/20 09:35:47 UTC

[netbeans] branch master updated: Separating unused element detection out of the semantic highlighter; adding a separate hint for unused elements.

This is an automated email from the ASF dual-hosted git repository.

neilcsmith pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/netbeans.git


The following commit(s) were added to refs/heads/master by this push:
     new c8ead04  Separating unused element detection out of the semantic highlighter; adding a separate hint for unused elements.
     new 5c5c23e  Merge pull request #2207 from jlahoda/unused-hint
c8ead04 is described below

commit c8ead044e5e6ab836e2996d3b42f2bc943868e7b
Author: Jan Lahoda <jl...@netbeans.org>
AuthorDate: Sun Jun 21 09:01:41 2020 +0200

    Separating unused element detection out of the semantic highlighter; adding a separate hint for unused elements.
---
 .../base/semantic/SemanticHighlighterBase.java     | 284 +-----------
 .../java/editor/base/semantic/UnusedDetector.java  | 486 +++++++++++++++++++++
 .../java/editor/base/semantic/Utilities.java       |  17 +
 .../base/semantic/DetectorTest/test89356.pass      |   2 +-
 .../DetectorTest/testConstructorUsedBySuper1.pass  |   4 +-
 .../DetectorTest/testConstructorUsedBySuper2.pass  |   4 +-
 .../DetectorTest/testLambdaAndFunctionType.pass    |   4 +-
 .../semantic/data/ConstructorUsedBySuper1.java     |   2 +-
 .../semantic/data/ConstructorUsedBySuper2.java     |   2 +-
 .../java/editor/base/semantic/DetectorTest.java    |   6 +-
 .../java/editor/base/semantic/TestBase.java        |  48 +-
 .../editor/base/semantic/UnusedDetectorTest.java   | 377 ++++++++++++++++
 .../netbeans/modules/java/hints/bugs/Unused.java   |  85 ++++
 .../modules/java/hints/bugs/UnusedTest.java        |  53 +++
 14 files changed, 1078 insertions(+), 296 deletions(-)

diff --git a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/SemanticHighlighterBase.java b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/SemanticHighlighterBase.java
index 00d3687..00e115d 100644
--- a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/SemanticHighlighterBase.java
+++ b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/SemanticHighlighterBase.java
@@ -18,15 +18,10 @@
  */
 package org.netbeans.modules.java.editor.base.semantic;
 
-import com.sun.source.tree.ArrayTypeTree;
-import com.sun.source.tree.AssignmentTree;
 import com.sun.source.tree.ClassTree;
 import com.sun.source.tree.CompilationUnitTree;
-import com.sun.source.tree.CompoundAssignmentTree;
-import com.sun.source.tree.EnhancedForLoopTree;
 import com.sun.source.tree.ExportsTree;
 import com.sun.source.tree.IdentifierTree;
-import com.sun.source.tree.LambdaExpressionTree;
 import com.sun.source.tree.LiteralTree;
 import com.sun.source.tree.MemberReferenceTree;
 import com.sun.source.tree.MemberSelectTree;
@@ -44,13 +39,11 @@ import com.sun.source.tree.UsesTree;
 import com.sun.source.tree.VariableTree;
 import com.sun.source.util.SourcePositions;
 import com.sun.source.util.TreePath;
-import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -63,13 +56,9 @@ import javax.lang.model.element.Element;
 import javax.lang.model.element.ElementKind;
 import javax.lang.model.element.ExecutableElement;
 import javax.lang.model.element.Modifier;
-import javax.lang.model.element.TypeElement;
-import javax.lang.model.type.TypeKind;
-import javax.lang.model.type.TypeMirror;
 import javax.swing.text.Document;
 import org.netbeans.api.java.lexer.JavaTokenId;
 import org.netbeans.api.java.source.CompilationInfo;
-import org.netbeans.api.java.source.ElementHandle;
 import org.netbeans.api.java.source.JavaParserResultTask;
 import org.netbeans.api.java.source.JavaSource.Phase;
 import org.netbeans.api.java.source.TreeUtilities;
@@ -77,15 +66,14 @@ import org.netbeans.api.java.source.support.CancellableTreePathScanner;
 import org.netbeans.api.lexer.PartType;
 import org.netbeans.api.lexer.Token;
 import org.netbeans.api.lexer.TokenHierarchy;
-//import org.netbeans.modules.editor.NbEditorUtilities;
 import org.netbeans.modules.java.editor.base.imports.UnusedImports;
 import org.netbeans.modules.java.editor.base.semantic.ColoringAttributes.Coloring;
+import org.netbeans.modules.java.editor.base.semantic.UnusedDetector.UnusedDescription;
 import org.netbeans.modules.parsing.spi.Parser.Result;
 import org.netbeans.modules.parsing.spi.Scheduler;
 import org.netbeans.modules.parsing.spi.SchedulerEvent;
 import org.netbeans.modules.parsing.spi.TaskIndexingMode;
 import org.openide.filesystems.FileUtil;
-import org.openide.util.Exceptions;
 import org.openide.util.Pair;
 
 
@@ -152,95 +140,6 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask {
         
     protected abstract boolean process(CompilationInfo info, final Document doc);
     
-    /**
-     * Signatures of Serializable methods.
-     */
-    private static final Set<String> SERIALIZABLE_SIGNATURES = new HashSet<>(Arrays.asList(new String[] {
-        "writeObject(Ljava/io/ObjectOutputStream;)V",
-        "readObject(Ljava/io/ObjectInputStream;)V",
-        "readResolve()Ljava/lang/Object;",
-        "writeReplace()Ljava/lang/Object;",
-        "readObjectNoData()V",
-    }));
-    
-    /**
-     * Also returns true on error / undecidable situation, so the filtering 
-     * will probably accept serial methods and will not mark them as unused, if
-     * the class declaration is errneous.
-     * 
-     * @param info the compilation context
-     * @param e the class member (the enclosing element will be tested)
-     * @return true, if in serializable/externalizable or unknown
-     */
-    private static boolean isInSerializableOrExternalizable(CompilationInfo info, Element e) {
-        Element encl = e.getEnclosingElement();
-        if (encl == null || !encl.getKind().isClass()) {
-            return true;
-        }
-        TypeMirror m = encl.asType();
-        if (m == null || m.getKind() != TypeKind.DECLARED) {
-            return true;
-        }
-        Element serEl = info.getElements().getTypeElement("java.io.Serializable"); // NOI18N
-        Element extEl = info.getElements().getTypeElement("java.io.Externalizable"); // NOI18N
-        if (serEl == null || extEl == null) {
-            return true;
-        }
-        if (info.getTypes().isSubtype(m, serEl.asType())) {
-            return true;
-        }
-        if (info.getTypes().isSubtype(m, extEl.asType())) {
-            return true;
-        }
-        return false;
-    }
-    
-    private static Field signatureAccessField;
-    
-    /**
-     * Hack to get signature out of ElementHandle - there's no API method for that
-     */
-    private static String _getSignatureHack(ElementHandle<ExecutableElement> eh) {
-        try {
-            if (signatureAccessField == null) {
-                try {
-                    Field f = ElementHandle.class.getDeclaredField("signatures"); // NOI18N
-                    f.setAccessible(true);
-                    signatureAccessField = f;
-                } catch (NoSuchFieldException | SecurityException ex) {
-                    // ignore
-                    return ""; // NOI18N
-                }
-            }
-            String[] signs = (String[])signatureAccessField.get(eh);
-            if (signs == null || signs.length != 3) {
-                return ""; // NOI18N
-            } else {
-                return signs[1] + signs[2];
-            }
-        } catch (IllegalArgumentException | IllegalAccessException ex) {
-            return ""; // NOI18N
-        }
-    }
-
-    /**
-     * Checks if the method is specified by Serialization API and the class
-     * extends Serializable/Externalizable. Unused methods defined in API spec
-     * should not be marked as unused.
-     * 
-     * @param info compilation context
-     * @param method the method
-     * @return true, if the method is from serialization API and should not be reported
-     */
-    private boolean isSerializationMethod(CompilationInfo info, ExecutableElement method) {
-        if (!isInSerializableOrExternalizable(info, method)) {
-            return false;
-        }
-        ElementHandle<ExecutableElement> eh = ElementHandle.create(method);
-        String sign = _getSignatureHack(eh);
-        return SERIALIZABLE_SIGNATURES.contains(sign);
-    }
-    
     protected boolean process(CompilationInfo info, final Document doc, ErrorDescriptionSetter setter) {
         DetectorVisitor v = new DetectorVisitor(info, doc, cancel);
         
@@ -277,6 +176,9 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask {
             }
         }
         
+        Map<Element, List<UnusedDescription>> element2Unused = UnusedDetector.findUnused(info) //XXX: unnecessarily ugly
+                                                                             .stream()
+                                                                             .collect(Collectors.groupingBy(ud -> ud.unusedElement));
         for (Element decl : v.type2Uses.keySet()) {
             if (cancel.get())
                 return true;
@@ -287,23 +189,9 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask {
                 if (u.spec == null)
                     continue;
                 
-                if (u.type.contains(UseTypes.DECLARATION) && Utilities.isPrivateElement(decl)) {
-                    if ((decl.getKind().isField() && !isSerialSpecField(info, decl)) || isLocalVariableClosure(decl)) {
-                        if (!hasAllTypes(uses, EnumSet.of(UseTypes.READ, UseTypes.WRITE))) {
-                            u.spec.add(ColoringAttributes.UNUSED);
-                        }
-                    }
-                    
-                    if ((decl.getKind() == ElementKind.CONSTRUCTOR && !decl.getModifiers().contains(Modifier.PRIVATE)) || decl.getKind() == ElementKind.METHOD) {
-                        if (!(hasAllTypes(uses, EnumSet.of(UseTypes.EXECUTE)) || isSerializationMethod(info, (ExecutableElement)decl))) {
-                            u.spec.add(ColoringAttributes.UNUSED);
-                        }
-                    }
-                    
-                    if (decl.getKind().isClass() || decl.getKind().isInterface()) {
-                        if (!hasAllTypes(uses, EnumSet.of(UseTypes.CLASS_USE))) {
-                            u.spec.add(ColoringAttributes.UNUSED);
-                        }
+                if (u.declaration) {
+                    if (element2Unused.containsKey(decl)) {
+                        u.spec.add(ColoringAttributes.UNUSED);
                     }
                 }
                 
@@ -336,23 +224,6 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask {
         return false;
     }
     
-        
-    private boolean hasAllTypes(List<Use> uses, EnumSet<UseTypes> types) {
-        for (Use u : uses) {
-            if (types.isEmpty()) {
-                return true;
-            }
-            
-            types.removeAll(u.type);
-        }
-        
-        return types.isEmpty();
-    }
-    
-    private enum UseTypes {
-        READ, WRITE, EXECUTE, DECLARATION, CLASS_USE, MODULE_USE;
-    }
-    
     private static Coloring collection2Coloring(Collection<ColoringAttributes> attr) {
         Coloring c = ColoringAttributes.empty();
         
@@ -383,41 +254,20 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask {
                LOCAL_VARIABLES.contains(el.getKind());
     }
     
-    /** Detects static final long SerialVersionUID 
-     * @return true if element is final static long serialVersionUID
-     */
-    private static boolean isSerialSpecField(CompilationInfo info, Element el) {
-        if (el.getModifiers().contains(Modifier.FINAL) 
-                && el.getModifiers().contains(Modifier.STATIC)) {
-            
-            if (!isInSerializableOrExternalizable(info, el)) {
-                return false;
-            }
-            if (info.getTypes().getPrimitiveType(TypeKind.LONG).equals(el.asType())
-                && el.getSimpleName().toString().equals("serialVersionUID")) {
-                return true;
-            }
-            if (el.getSimpleName().contentEquals("serialPersistentFields")) {
-                return true;
-            }
-        }
-        return false;
-    }
-        
     private static class Use {
-        private Collection<UseTypes> type;
+        private boolean declaration;
         private TreePath     tree;
         private Collection<ColoringAttributes> spec;
         
-        public Use(Collection<UseTypes> type, TreePath tree, Collection<ColoringAttributes> spec) {
-            this.type = type;
+        public Use(boolean declaration, TreePath tree, Collection<ColoringAttributes> spec) {
+            this.declaration = declaration;
             this.tree = tree;
             this.spec = spec;
         }
         
         @Override
         public String toString() {
-            return "Use: " + type;
+            return "Use: " + spec;
         }
     }
     
@@ -582,23 +432,6 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask {
             return null;
         }
 
-        private Element toRecordComponent(Element el) {
-            if (el == null ||el.getKind() != ElementKind.FIELD) {
-                return el;
-            }
-            TypeElement owner = (TypeElement) el.getEnclosingElement();
-            if (!"RECORD".equals(owner.getKind().name())) {
-                return el;
-            }
-            for (Element encl : owner.getEnclosedElements()) {
-                if (TreeShims.isRecordComponent(encl.getKind()) &&
-                    encl.getSimpleName().equals(el.getSimpleName())) {
-                    return encl;
-                }
-            }
-            return el;
-        }
-
         private static final Set<Kind> LITERALS = EnumSet.of(Kind.BOOLEAN_LITERAL, Kind.CHAR_LITERAL, Kind.DOUBLE_LITERAL, Kind.FLOAT_LITERAL, Kind.INT_LITERAL, Kind.LONG_LITERAL, Kind.STRING_LITERAL);
 
         private void handlePossibleIdentifier(TreePath expr, boolean declaration) {
@@ -621,7 +454,7 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask {
                 return ;
             }
 
-            decl = decl == null ? toRecordComponent(info.getTrees().getElement(expr)) : decl;
+            decl = decl == null ? Utilities.toRecordComponent(info.getTrees().getElement(expr)) : decl;
 
             ElementKind declKind = decl != null ? decl.getKind() : null;
             boolean isDeclType = decl != null &&
@@ -689,98 +522,23 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask {
             }
             
             if (c != null) {
-                Collection<UseTypes> type = EnumSet.noneOf(UseTypes.class);
-
-                if (isDeclType) {
-                    if (!declaration) {
-                        type.add(UseTypes.CLASS_USE);
+                if (decl.getKind() == ElementKind.CONSTRUCTOR && !declaration) {
+                    if (info.getElements().isDeprecated(decl.getEnclosingElement())) {
+                        c.add(ColoringAttributes.DEPRECATED);
                     }
-                } else if (decl.getKind().isField() || isLocalVariableClosure(decl)) {
-                    if (!declaration) {
-                        while (true) {
-                            if (parent.getLeaf().getKind() == Kind.POSTFIX_DECREMENT ||
-                                parent.getLeaf().getKind() == Kind.POSTFIX_INCREMENT ||
-                                parent.getLeaf().getKind() == Kind.PREFIX_DECREMENT ||
-                                parent.getLeaf().getKind() == Kind.PREFIX_INCREMENT) {
-                                type.add(UseTypes.WRITE);
-                                currentPath = parent;
-                                parent = currentPath.getParentPath();
-                                continue;
-                            }
-                            if (CompoundAssignmentTree.class.isAssignableFrom(parent.getLeaf().getKind().asInterface()) &&
-                                ((CompoundAssignmentTree) parent.getLeaf()).getVariable() == currentPath.getLeaf()) {
-                                type.add(UseTypes.WRITE);
-                                currentPath = parent;
-                                parent = currentPath.getParentPath();
-                                continue;
-                            }
-                            break;
-                        }
-                        if (parent.getLeaf().getKind() == Kind.ASSIGNMENT &&
-                            ((AssignmentTree) parent.getLeaf()).getVariable() == currentPath.getLeaf()) {
-                            type.add(UseTypes.WRITE);
-                        } else if (parent.getLeaf().getKind() != Kind.EXPRESSION_STATEMENT) {
-                            type.add(UseTypes.READ);
-                        }
-                    } else if (decl.getKind() == ElementKind.PARAMETER) {
-                        Element method = decl.getEnclosingElement();
-
-                        type.add(UseTypes.WRITE);
-
-                        if (parent.getLeaf().getKind() == Kind.LAMBDA_EXPRESSION &&
-                            ((LambdaExpressionTree) parent.getLeaf()).getParameters().contains(currentPath.getLeaf())) {
-//                            type.add(UseTypes.READ);
-                        } else if (method.getModifiers().contains(Modifier.ABSTRACT) || method.getModifiers().contains(Modifier.NATIVE) || !method.getModifiers().contains(Modifier.PRIVATE)) {
-                            type.add(UseTypes.READ);
-                        }
-                    } else if (decl.getKind().isField() || decl.getKind() == ElementKind.EXCEPTION_PARAMETER || decl.getKind() == BINDING_VARIABLE) {
-                        type.add(UseTypes.WRITE);
-                    } else if (parent.getLeaf().getKind() == Kind.ENHANCED_FOR_LOOP &&
-                               ((EnhancedForLoopTree) parent.getLeaf()).getVariable() == currentPath.getLeaf()) {
-                        type.add(UseTypes.WRITE);
-                    } else {
-                        VariableTree vt = (VariableTree) currentPath.getLeaf();
-
-                        if (vt.getInitializer() != null) {
-                            type.add(UseTypes.WRITE);
-                        }
-                    }
-                } else if (decl.getKind() == ElementKind.METHOD) {
-                    if (!declaration) {
-                        type.add(UseTypes.EXECUTE);
-                    }
-                } else if (decl.getKind() == ElementKind.CONSTRUCTOR) {
-                    if (!declaration) {
-                        if (info.getElements().isDeprecated(decl.getEnclosingElement())) {
-                            c.add(ColoringAttributes.DEPRECATED);
-                        }
-                        type.add(UseTypes.EXECUTE);
-                    }
-                } else if (TreeShims.isRecordComponent(toRecordComponent(decl).getKind())) {
-                    if (declaration) {
-                        type.add(UseTypes.READ);
-                        type.add(UseTypes.WRITE);
-                    }
-                }
-                if (declaration) {
-                    type.add(UseTypes.DECLARATION);
                 }
-                addUse(decl, type, expr, c);
+                addUse(decl, declaration, expr, c);
             }
         }
         
-        private void addUse(Element decl, Collection<UseTypes> useTypes, TreePath t, Collection<ColoringAttributes> c) {
-            if (decl == recursionDetector) {
-                useTypes.remove(UseTypes.EXECUTE); //recursive execution is not use
-            }
-            
+        private void addUse(Element decl, boolean declaration, TreePath t, Collection<ColoringAttributes> c) {
             List<Use> uses = type2Uses.get(decl);
             
             if (uses == null) {
                 type2Uses.put(decl, uses = new ArrayList<Use>());
             }
             
-            Use u = new Use(useTypes, t, c);
+            Use u = new Use(declaration, t, c);
             
             uses.add(u);
         }
@@ -924,7 +682,7 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask {
                 if ("super".equals(ident) || "this".equals(ident)) { //NOI18N
                     Element resolved = info.getTrees().getElement(getCurrentPath());
                     
-                    addUse(resolved, EnumSet.of(UseTypes.EXECUTE), null, null);
+                    addUse(resolved, false, null, null);
                 }
             }
             
@@ -1076,7 +834,7 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask {
             Element clazz = info.getTrees().getElement(tp);
             
             if (clazz != null) {
-                addUse(clazz, EnumSet.of(UseTypes.CLASS_USE), null, null);
+                addUse(clazz, false, null, null);
             }
 	    
             scan(tree.getEnclosingExpression(), null);
@@ -1133,7 +891,7 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask {
         
         private boolean isRecordComponent(Tree member) {
             Element el = info.getTrees().getElement(new TreePath(getCurrentPath(), member));
-            return el != null && TreeShims.isRecordComponent(toRecordComponent(el).getKind());
+            return el != null && TreeShims.isRecordComponent(Utilities.toRecordComponent(el).getKind());
         }
 
         @Override
diff --git a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/UnusedDetector.java b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/UnusedDetector.java
new file mode 100644
index 0000000..a458ba5
--- /dev/null
+++ b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/UnusedDetector.java
@@ -0,0 +1,486 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.netbeans.modules.java.editor.base.semantic;
+
+import com.sun.source.tree.AssignmentTree;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.CompoundAssignmentTree;
+import com.sun.source.tree.EnhancedForLoopTree;
+import com.sun.source.tree.IdentifierTree;
+import com.sun.source.tree.MemberReferenceTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.NewClassTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.Tree.Kind;
+import com.sun.source.tree.VariableTree;
+import com.sun.source.util.TreePath;
+import com.sun.source.util.TreePathScanner;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import org.netbeans.api.java.source.CompilationInfo;
+import org.netbeans.api.java.source.ElementHandle;
+import org.netbeans.api.java.source.support.ErrorAwareTreePathScanner;
+
+/**
+ *
+ * @author lahvac
+ */
+public class UnusedDetector {
+
+    public static class UnusedDescription {
+        public final Element unusedElement;
+        public final TreePath unusedElementPath;
+        public final UnusedReason reason;
+
+        public UnusedDescription(Element unusedElement, TreePath unusedElementPath, UnusedReason reason) {
+            this.unusedElement = unusedElement;
+            this.unusedElementPath = unusedElementPath;
+            this.reason = reason;
+        }
+
+    }
+
+    public enum UnusedReason {
+        NOT_WRITTEN_READ("neither read or written to"),
+        NOT_WRITTEN("never written to"), //makes sense?
+        NOT_READ("never read"),
+        NOT_USED("never used");
+        private final String text;
+
+        private UnusedReason(String text) {
+            this.text = text;
+        }
+
+    }
+
+    public static List<UnusedDescription> findUnused(CompilationInfo info) {
+        List<UnusedDescription> cached = (List<UnusedDescription>) info.getCachedValue(UnusedDetector.class);
+        if (cached != null) {
+            return cached;
+        }
+
+        UnusedVisitor uv = new UnusedVisitor(info);
+        uv.scan(info.getCompilationUnit(), null);
+        List<UnusedDescription> result = new ArrayList<>();
+        for (Entry<Element, TreePath> e : uv.element2Declaration.entrySet()) {
+            Element el = e.getKey();
+            TreePath declaration = e.getValue();
+            Set<UseTypes> uses = uv.useTypes.getOrDefault(el, Collections.emptySet());
+            boolean isPrivate = el.getModifiers().contains(Modifier.PRIVATE); //TODO: effectivelly private!
+            if (isLocalVariableClosure(el) || (el.getKind().isField() && isPrivate)) {
+                if (!isSerialSpecField(info, el)) {
+                    boolean isWritten = uses.contains(UseTypes.WRITTEN);
+                    boolean isRead = uses.contains(UseTypes.READ);
+                    if (!isWritten && !isRead) {
+                        result.add(new UnusedDescription(el, declaration, UnusedReason.NOT_WRITTEN_READ));
+                    } else if (!isWritten) {
+                        result.add(new UnusedDescription(el, declaration, UnusedReason.NOT_WRITTEN));
+                    } else if (!isRead) {
+                        result.add(new UnusedDescription(el, declaration, UnusedReason.NOT_READ));
+                    }
+                }
+            } else if ((el.getKind() == ElementKind.CONSTRUCTOR || el.getKind() == ElementKind.METHOD) && isPrivate) {
+                if (!isSerializationMethod(info, (ExecutableElement)el) && !uses.contains(UseTypes.USED)) {
+                    result.add(new UnusedDescription(el, declaration, UnusedReason.NOT_USED));
+                }
+            } else if ((el.getKind().isClass() || el.getKind().isInterface()) && isPrivate) {
+                if (!uses.contains(UseTypes.USED)) {
+                    result.add(new UnusedDescription(el, declaration, UnusedReason.NOT_USED));
+                }
+            }
+        }
+
+        info.putCachedValue(UnusedDetector.class, result, CompilationInfo.CacheClearPolicy.ON_CHANGE);
+
+        return result;
+    }
+
+    /** Detects static final long SerialVersionUID
+     * @return true if element is final static long serialVersionUID
+     */
+    private static boolean isSerialSpecField(CompilationInfo info, Element el) {
+        if (el.getModifiers().contains(Modifier.FINAL)
+                && el.getModifiers().contains(Modifier.STATIC)) {
+
+            if (!isInSerializableOrExternalizable(info, el)) {
+                return false;
+            }
+            if (info.getTypes().getPrimitiveType(TypeKind.LONG).equals(el.asType())
+                && el.getSimpleName().toString().equals("serialVersionUID")) {
+                return true;
+            }
+            if (el.getSimpleName().contentEquals("serialPersistentFields")) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Also returns true on error / undecidable situation, so the filtering
+     * will probably accept serial methods and will not mark them as unused, if
+     * the class declaration is errneous.
+     *
+     * @param info the compilation context
+     * @param e the class member (the enclosing element will be tested)
+     * @return true, if in serializable/externalizable or unknown
+     */
+    private static boolean isInSerializableOrExternalizable(CompilationInfo info, Element e) {
+        Element encl = e.getEnclosingElement();
+        if (encl == null || !encl.getKind().isClass()) {
+            return true;
+        }
+        TypeMirror m = encl.asType();
+        if (m == null || m.getKind() != TypeKind.DECLARED) {
+            return true;
+        }
+        Element serEl = info.getElements().getTypeElement("java.io.Serializable"); // NOI18N
+        Element extEl = info.getElements().getTypeElement("java.io.Externalizable"); // NOI18N
+        if (serEl == null || extEl == null) {
+            return true;
+        }
+        if (info.getTypes().isSubtype(m, serEl.asType())) {
+            return true;
+        }
+        if (info.getTypes().isSubtype(m, extEl.asType())) {
+            return true;
+        }
+        return false;
+    }
+
+    private static Field signatureAccessField;
+
+    /**
+     * Hack to get signature out of ElementHandle - there's no API method for that
+     */
+    private static String _getSignatureHack(ElementHandle<ExecutableElement> eh) {
+        try {
+            if (signatureAccessField == null) {
+                try {
+                    Field f = ElementHandle.class.getDeclaredField("signatures"); // NOI18N
+                    f.setAccessible(true);
+                    signatureAccessField = f;
+                } catch (NoSuchFieldException | SecurityException ex) {
+                    // ignore
+                    return ""; // NOI18N
+                }
+            }
+            String[] signs = (String[])signatureAccessField.get(eh);
+            if (signs == null || signs.length != 3) {
+                return ""; // NOI18N
+            } else {
+                return signs[1] + signs[2];
+            }
+        } catch (IllegalArgumentException | IllegalAccessException ex) {
+            return ""; // NOI18N
+        }
+    }
+
+    /**
+     * Checks if the method is specified by Serialization API and the class
+     * extends Serializable/Externalizable. Unused methods defined in API spec
+     * should not be marked as unused.
+     *
+     * @param info compilation context
+     * @param method the method
+     * @return true, if the method is from serialization API and should not be reported
+     */
+    private static boolean isSerializationMethod(CompilationInfo info, ExecutableElement method) {
+        if (!isInSerializableOrExternalizable(info, method)) {
+            return false;
+        }
+        ElementHandle<ExecutableElement> eh = ElementHandle.create(method);
+        String sign = _getSignatureHack(eh);
+        return SERIALIZABLE_SIGNATURES.contains(sign);
+    }
+
+    /**
+     * Signatures of Serializable methods.
+     */
+    private static final Set<String> SERIALIZABLE_SIGNATURES = new HashSet<>(Arrays.asList(new String[] {
+        "writeObject(Ljava/io/ObjectOutputStream;)V",
+        "readObject(Ljava/io/ObjectInputStream;)V",
+        "readResolve()Ljava/lang/Object;",
+        "writeReplace()Ljava/lang/Object;",
+        "readObjectNoData()V",
+    }));
+
+    private static final Set<ElementKind> LOCAL_VARIABLES = EnumSet.of(
+            ElementKind.LOCAL_VARIABLE, ElementKind.RESOURCE_VARIABLE,
+            ElementKind.EXCEPTION_PARAMETER);
+    private static final ElementKind BINDING_VARIABLE;
+
+    static {
+        ElementKind bindingVariable;
+        try {
+            LOCAL_VARIABLES.add(bindingVariable = ElementKind.valueOf(TreeShims.BINDING_VARIABLE));
+        } catch (IllegalArgumentException ex) {
+            bindingVariable = null;
+        }
+        BINDING_VARIABLE = bindingVariable;
+    }
+
+    private static boolean isLocalVariableClosure(Element el) {
+        return el.getKind() == ElementKind.PARAMETER ||
+               LOCAL_VARIABLES.contains(el.getKind());
+    }
+
+    private enum UseTypes {
+        READ, WRITTEN, USED;
+    }
+
+    private static final class UnusedVisitor extends ErrorAwareTreePathScanner<Void, Void> {
+
+        private final Map<Element, Set<UseTypes>> useTypes = new HashMap<>();
+        private final Map<Element, TreePath> element2Declaration = new HashMap<>();
+        private final CompilationInfo info;
+        private ExecutableElement recursionDetector;
+
+        public UnusedVisitor(CompilationInfo info) {
+            this.info = info;
+        }
+
+        @Override
+        public Void visitIdentifier(IdentifierTree node, Void p) {
+            handleUse();
+            return super.visitIdentifier(node, p);
+        }
+
+        @Override
+        public Void visitMemberSelect(MemberSelectTree node, Void p) {
+            handleUse();
+            return super.visitMemberSelect(node, p);
+        }
+
+        @Override
+        public Void visitMemberReference(MemberReferenceTree node, Void p) {
+            handleUse();
+            return super.visitMemberReference(node, p);
+        }
+
+        @Override
+        public Void visitNewClass(NewClassTree node, Void p) {
+            handleUse();
+            return super.visitNewClass(node, p);
+        }
+
+        private void handleUse() {
+            Element el = info.getTrees().getElement(getCurrentPath());
+
+            if (el == null) {
+                return ;
+            }
+
+            boolean isPrivate = el.getModifiers().contains(Modifier.PRIVATE); //TODO: effectivelly private!
+
+            if (isLocalVariableClosure(el) || (el.getKind().isField() && isPrivate)) {
+                TreePath effectiveUse = getCurrentPath();
+                boolean isWrite = false;
+                boolean isRead = false;
+
+                OUTER: while (true) {
+                    TreePath parent = effectiveUse.getParentPath();
+
+                    switch (parent.getLeaf().getKind()) {
+                        case ASSIGNMENT:
+                            AssignmentTree at = (AssignmentTree) parent.getLeaf();
+                            if (at.getVariable() == effectiveUse.getLeaf()) {
+                                isWrite = true;
+                            } else if (at.getExpression() == effectiveUse.getLeaf()) {
+                                isRead = true;
+                            }
+                            break OUTER;
+                        case AND_ASSIGNMENT: case DIVIDE_ASSIGNMENT:
+                        case LEFT_SHIFT_ASSIGNMENT: case MINUS_ASSIGNMENT:
+                        case MULTIPLY_ASSIGNMENT: case OR_ASSIGNMENT:
+                        case PLUS_ASSIGNMENT: case REMAINDER_ASSIGNMENT:
+                        case RIGHT_SHIFT_ASSIGNMENT: case UNSIGNED_RIGHT_SHIFT_ASSIGNMENT:
+                        case XOR_ASSIGNMENT:
+                            CompoundAssignmentTree cat = (CompoundAssignmentTree) parent.getLeaf();
+                            if (cat.getVariable() == effectiveUse.getLeaf()) {
+                                //check if the results of compound assignment is used:
+                                effectiveUse = parent;
+                                break;
+                            }
+                            //use on the right hand side of the compound assignment - consider as read
+                            isRead = true;
+                            break OUTER;
+                        case EXPRESSION_STATEMENT:
+                            break OUTER;
+                        default:
+                            isRead = true;
+                            break OUTER;
+                    }
+                }
+
+                if (isWrite) {
+                    addUse(el, UseTypes.WRITTEN);
+                }
+
+                if (isRead) {
+                    addUse(el, UseTypes.READ);
+                }
+            } else if (isPrivate) {
+                if (el.getKind() != ElementKind.METHOD || recursionDetector != el)
+                addUse(el, UseTypes.USED);
+            }
+        }
+
+        private void addUse(Element el, UseTypes type) {
+            useTypes.computeIfAbsent(el, x -> EnumSet.noneOf(UseTypes.class)).add(type);
+        }
+
+        @Override
+        public Void visitClass(ClassTree node, Void p) {
+            ExecutableElement prevRecursionDetector = recursionDetector;
+
+            try {
+                recursionDetector = null;
+
+                handleDeclaration(getCurrentPath());
+                return super.visitClass(node, p);
+            } finally {
+                recursionDetector = prevRecursionDetector;
+            }
+        }
+
+        @Override
+        public Void visitVariable(VariableTree node, Void p) {
+            handleDeclaration(getCurrentPath());
+            return super.visitVariable(node, p);
+        }
+
+        @Override
+        public Void visitMethod(MethodTree node, Void p) {
+            ExecutableElement prevRecursionDetector = recursionDetector;
+
+            try {
+                Element el = info.getTrees().getElement(getCurrentPath());
+                recursionDetector = (el != null && el.getKind() == ElementKind.METHOD) ? (ExecutableElement) el : null;
+                handleDeclaration(getCurrentPath());
+                return super.visitMethod(node, p);
+            } finally {
+                recursionDetector = prevRecursionDetector;
+            }
+        }
+
+        @Override
+        public Void scan(Tree tree, Void p) {
+            if (tree != null && TreeShims.BINDING_PATTERN.equals(tree.getKind().name())) {
+                handleDeclaration(new TreePath(getCurrentPath(), tree));
+            }
+            return super.scan(tree, p);
+        }
+
+        private void handleDeclaration(TreePath path) {
+            Element el = info.getTrees().getElement(path);
+
+            if (el == null) {
+                return ;
+            }
+
+            element2Declaration.put(el, path);
+
+            if (el.getKind() == ElementKind.PARAMETER) {
+                addUse(el, UseTypes.WRITTEN);
+                boolean read = true;
+                Tree parent = path.getParentPath().getLeaf();
+                if (parent.getKind() == Kind.METHOD) {
+                    MethodTree method = (MethodTree) parent;
+                    Set<Modifier> mods = method.getModifiers().getFlags();
+                    if (method.getParameters().contains(path.getLeaf()) &&
+                        mods.contains(Modifier.PRIVATE) && !mods.contains(Modifier.ABSTRACT) &&
+                        !mods.contains(Modifier.NATIVE)) {
+                        read = false;
+                    }
+                }
+                if (read) {
+                    addUse(el, UseTypes.READ);
+                }
+            } else if (el.getKind() == ElementKind.EXCEPTION_PARAMETER) {
+                //Ignore unread caught exceptions. There are valid reasons why it
+                //could be unused; and there should be a separate hint checking if
+                //it makes sense to no use it:
+                addUse(el, UseTypes.READ);
+                addUse(el, UseTypes.WRITTEN);
+            } else if (el.getKind() == BINDING_VARIABLE) {
+                addUse(el, UseTypes.WRITTEN);
+            } else if (el.getKind() == ElementKind.LOCAL_VARIABLE) {
+                Tree parent = path.getParentPath().getLeaf();
+                if (parent.getKind() == Kind.ENHANCED_FOR_LOOP &&
+                    ((EnhancedForLoopTree) parent).getVariable() == path.getLeaf()) {
+                    addUse(el, UseTypes.WRITTEN);
+                }
+            } else if (TreeShims.isRecordComponent(Utilities.toRecordComponent(el).getKind())) {
+                addUse(el, UseTypes.READ);
+                addUse(el, UseTypes.WRITTEN);
+            } else if (el.getKind().isField()) {
+                addUse(el, UseTypes.WRITTEN);
+            } else if (el.getKind() == ElementKind.CONSTRUCTOR &&
+                       el.getModifiers().contains(Modifier.PRIVATE) &&
+                       ((ExecutableElement) el).getParameters().isEmpty()) {
+                //check if this constructor prevent initalization of "utility" class,
+                //in which case, it is not "unused":
+                TypeElement encl = (TypeElement) el.getEnclosingElement();
+                TypeElement jlObject = info.getElements().getTypeElement("java.lang.Object");
+                boolean utility = !encl.getModifiers().contains(Modifier.ABSTRACT) &&
+                                  encl.getInterfaces().isEmpty() &&
+                                  (jlObject == null || info.getTypes().isSameType(encl.getSuperclass(), jlObject.asType()));
+                for (Element sibling : el.getEnclosingElement().getEnclosedElements()) {
+                    if (sibling.getKind() == ElementKind.CONSTRUCTOR && !sibling.equals(el)) {
+                        utility = false;
+                        break;
+                    } else if ((sibling.getKind().isField() || sibling.getKind() == ElementKind.METHOD) &&
+                                !sibling.getModifiers().contains(Modifier.STATIC)) {
+                        utility = false;
+                        break;
+                    }
+                }
+                if (utility) {
+                    addUse(el, UseTypes.USED);
+                }
+            }
+
+            if (path.getLeaf().getKind() == Kind.VARIABLE) {
+                VariableTree vt = (VariableTree) path.getLeaf();
+                if (vt.getInitializer() != null) {
+                    addUse(el, UseTypes.WRITTEN);
+                }
+            }
+        }
+    }
+}
diff --git a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/Utilities.java b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/Utilities.java
index e741427..bce68e1 100644
--- a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/Utilities.java
+++ b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/Utilities.java
@@ -56,6 +56,7 @@ import javax.lang.model.element.Name;
 import javax.tools.Diagnostic;
 
 import com.sun.source.tree.ModifiersTree;
+import javax.lang.model.element.TypeElement;
 import org.netbeans.api.java.lexer.JavaTokenId;
 import org.netbeans.api.java.source.TreeUtilities;
 import org.netbeans.api.lexer.Token;
@@ -722,5 +723,21 @@ public class Utilities {
         return LOCAL_ELEMENT_KINDS.contains(el.getKind()) || el.getModifiers().contains(Modifier.PRIVATE);
     }
 
+    public static Element toRecordComponent(Element el) {
+        if (el == null ||el.getKind() != ElementKind.FIELD) {
+            return el;
+        }
+        TypeElement owner = (TypeElement) el.getEnclosingElement();
+        if (!"RECORD".equals(owner.getKind().name())) {
+            return el;
+        }
+        for (Element encl : owner.getEnclosedElements()) {
+            if (TreeShims.isRecordComponent(encl.getKind()) &&
+                encl.getSimpleName().equals(el.getSimpleName())) {
+                return encl;
+            }
+        }
+        return el;
+    }
 
 }
diff --git a/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/test89356.pass b/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/test89356.pass
index 82e4188..d660bf4 100644
--- a/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/test89356.pass
+++ b/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/test89356.pass
@@ -2,4 +2,4 @@
 [PUBLIC, CLASS, DECLARATION], 4:13-4:34
 [PUBLIC, INTERFACE], 4:46-4:58
 [STATIC, PRIVATE, FIELD, DECLARATION], 5:30-5:46
-[PRIVATE, CONSTRUCTOR, DECLARATION], 7:12-7:33
+[PRIVATE, CONSTRUCTOR, UNUSED, DECLARATION], 7:12-7:33
diff --git a/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/testConstructorUsedBySuper1.pass b/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/testConstructorUsedBySuper1.pass
index cb4b9b5..d86206d 100644
--- a/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/testConstructorUsedBySuper1.pass
+++ b/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/testConstructorUsedBySuper1.pass
@@ -2,7 +2,7 @@
 [PRIVATE, CONSTRUCTOR, DECLARATION], 4:12-4:35
 [STATIC, PUBLIC, CLASS, DECLARATION], 8:24-8:32
 [PUBLIC, CLASS], 8:41-8:64
-[PRIVATE, CONSTRUCTOR, DECLARATION], 9:16-9:24
+[PUBLIC, CONSTRUCTOR, DECLARATION], 9:15-9:23
 [STATIC, PUBLIC, CLASS], 13:22-13:30
 [STATIC, PUBLIC, METHOD, DECLARATION], 13:31-13:37
-[PRIVATE, CONSTRUCTOR], 14:23-14:31
+[PUBLIC, CONSTRUCTOR], 14:23-14:31
diff --git a/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/testConstructorUsedBySuper2.pass b/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/testConstructorUsedBySuper2.pass
index cb4b9b5..d86206d 100644
--- a/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/testConstructorUsedBySuper2.pass
+++ b/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/testConstructorUsedBySuper2.pass
@@ -2,7 +2,7 @@
 [PRIVATE, CONSTRUCTOR, DECLARATION], 4:12-4:35
 [STATIC, PUBLIC, CLASS, DECLARATION], 8:24-8:32
 [PUBLIC, CLASS], 8:41-8:64
-[PRIVATE, CONSTRUCTOR, DECLARATION], 9:16-9:24
+[PUBLIC, CONSTRUCTOR, DECLARATION], 9:15-9:23
 [STATIC, PUBLIC, CLASS], 13:22-13:30
 [STATIC, PUBLIC, METHOD, DECLARATION], 13:31-13:37
-[PRIVATE, CONSTRUCTOR], 14:23-14:31
+[PUBLIC, CONSTRUCTOR], 14:23-14:31
diff --git a/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/testLambdaAndFunctionType.pass b/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/testLambdaAndFunctionType.pass
index ca70386..3a44978 100644
--- a/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/testLambdaAndFunctionType.pass
+++ b/java/java.editor.base/test/unit/data/goldenfiles/org/netbeans/modules/java/editor/base/semantic/DetectorTest/testLambdaAndFunctionType.pass
@@ -15,7 +15,7 @@
 [PUBLIC, CLASS], 9:40-9:46
 [PARAMETER, DECLARATION], 9:47-9:49
 [PUBLIC, CLASS], 9:51-9:57
-[PARAMETER, UNUSED, DECLARATION], 9:58-9:60
+[PARAMETER, DECLARATION], 9:58-9:60
 [PARAMETER], 9:73-9:75
 [PUBLIC, METHOD], 9:76-9:85
 [PUBLIC, CLASS], 10:8-10:19
@@ -24,7 +24,7 @@
 [PUBLIC, CLASS], 10:40-10:46
 [PARAMETER, DECLARATION], 10:47-10:49
 [PUBLIC, CLASS], 10:51-10:57
-[PARAMETER, UNUSED, DECLARATION], 10:58-10:60
+[PARAMETER, DECLARATION], 10:58-10:60
 [PARAMETER], 10:65-10:67
 [PUBLIC, METHOD], 10:68-10:77
 [PUBLIC, CLASS], 11:8-11:19
diff --git a/java/java.editor.base/test/unit/data/org/netbeans/modules/java/editor/base/semantic/data/ConstructorUsedBySuper1.java b/java/java.editor.base/test/unit/data/org/netbeans/modules/java/editor/base/semantic/data/ConstructorUsedBySuper1.java
index 172bbc5..eb0bdee 100644
--- a/java/java.editor.base/test/unit/data/org/netbeans/modules/java/editor/base/semantic/data/ConstructorUsedBySuper1.java
+++ b/java/java.editor.base/test/unit/data/org/netbeans/modules/java/editor/base/semantic/data/ConstructorUsedBySuper1.java
@@ -7,7 +7,7 @@ public class ConstructorUsedBySuper1 {
     }
     
     public static class Subclass extends ConstructorUsedBySuper1 {
-        private Subclass() {
+        public Subclass() {
             
         }
         
diff --git a/java/java.editor.base/test/unit/data/org/netbeans/modules/java/editor/base/semantic/data/ConstructorUsedBySuper2.java b/java/java.editor.base/test/unit/data/org/netbeans/modules/java/editor/base/semantic/data/ConstructorUsedBySuper2.java
index 7b99680..55bcc15 100644
--- a/java/java.editor.base/test/unit/data/org/netbeans/modules/java/editor/base/semantic/data/ConstructorUsedBySuper2.java
+++ b/java/java.editor.base/test/unit/data/org/netbeans/modules/java/editor/base/semantic/data/ConstructorUsedBySuper2.java
@@ -7,7 +7,7 @@ public class ConstructorUsedBySuper2 {
     }
     
     public static class Subclass extends ConstructorUsedBySuper2 {
-        private Subclass() {
+        public Subclass() {
             super();
         }
         
diff --git a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/DetectorTest.java b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/DetectorTest.java
index 03cd5e6..726b65c 100644
--- a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/DetectorTest.java
+++ b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/DetectorTest.java
@@ -515,7 +515,7 @@ public class DetectorTest extends TestBase {
             //OK, no RELEASE_14, skip tests
             return ;
         }
-        setSourceLevel("14");
+        enablePreview();
         performTest("Record",
                     "public record Test(String s) {}\n" +
                     "class T {\n" +
@@ -542,8 +542,8 @@ public class DetectorTest extends TestBase {
         } catch (IllegalArgumentException ex) {
             //OK, no RELEASE_14, skip tests
             return;
-        }        
-        setSourceLevel("14");
+        }
+        enablePreview();
         performTest("Records",
                     "public class Records {\n" +
                     "    public interface Super {}\n" +
diff --git a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/TestBase.java b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/TestBase.java
index 1ef26b6..6164663 100644
--- a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/TestBase.java
+++ b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/TestBase.java
@@ -167,26 +167,7 @@ public abstract class TestBase extends NbTestCase {
     protected void performTest(Input input, final Performer performer, boolean doCompileRecursively, Validator validator) throws Exception {
         SourceUtilsTestUtil.prepareTest(new String[] {"org/netbeans/modules/java/editor/resources/layer.xml"}, new Object[] {
             new MIMEResolverImpl(),
-            new CompilerOptionsQueryImplementation() {
-                @Override
-                public CompilerOptionsQueryImplementation.Result getOptions(FileObject file) {
-                    if (testSourceFO == file) {
-                        return new CompilerOptionsQueryImplementation.Result() {
-                            @Override
-                            public List<? extends String> getArguments() {
-                                return extraOptions;
-                            }
-
-                            @Override
-                            public void addChangeListener(ChangeListener listener) {}
-
-                            @Override
-                            public void removeChangeListener(ChangeListener listener) {}
-                        };
-                    }
-                    return null;
-                }
-            }
+            new CompilerOptionsQueryImplementationImpl()
         });
         
 	FileObject scratch = SourceUtilsTestUtil.makeScratchDir(this);
@@ -263,7 +244,10 @@ public abstract class TestBase extends NbTestCase {
     }
 
     protected void performTest(String fileName, String code, Performer performer, boolean doCompileRecursively, String... expected) throws Exception {
-        SourceUtilsTestUtil.prepareTest(new String[] {"org/netbeans/modules/java/editor/resources/layer.xml"}, new Object[] {new MIMEResolverImpl()});
+        SourceUtilsTestUtil.prepareTest(new String[] {"org/netbeans/modules/java/editor/resources/layer.xml"}, new Object[] {
+            new MIMEResolverImpl(),
+            new CompilerOptionsQueryImplementationImpl()
+        });
 
 	FileObject scratch = SourceUtilsTestUtil.makeScratchDir(this);
 	FileObject cache   = scratch.createFolder("cache");
@@ -426,4 +410,26 @@ public abstract class TestBase extends NbTestCase {
     protected interface Validator {
         public void validate(String actual) throws Exception;
     }
+
+    private class CompilerOptionsQueryImplementationImpl implements CompilerOptionsQueryImplementation {
+
+        @Override
+        public CompilerOptionsQueryImplementation.Result getOptions(FileObject file) {
+            if (testSourceFO == file || testSourceFO.getParent() == file) {
+                return new CompilerOptionsQueryImplementation.Result() {
+                    @Override
+                    public List<? extends String> getArguments() {
+                        return extraOptions;
+                    }
+
+                    @Override
+                    public void addChangeListener(ChangeListener listener) {}
+
+                    @Override
+                    public void removeChangeListener(ChangeListener listener) {}
+                };
+            }
+            return null;
+        }
+    }
 }
diff --git a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/UnusedDetectorTest.java b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/UnusedDetectorTest.java
new file mode 100644
index 0000000..e7bc89b
--- /dev/null
+++ b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/UnusedDetectorTest.java
@@ -0,0 +1,377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.netbeans.modules.java.editor.base.semantic;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.stream.Collectors;
+import javax.lang.model.SourceVersion;
+import javax.swing.text.Document;
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNotNull;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.netbeans.api.java.lexer.JavaTokenId;
+import org.netbeans.api.java.source.CompilationController;
+import org.netbeans.api.java.source.CompilationInfo;
+import org.netbeans.api.java.source.JavaSource;
+import org.netbeans.api.java.source.SourceUtilsTestUtil;
+import org.netbeans.api.java.source.TestUtilities;
+import org.netbeans.api.java.source.Task;
+import org.netbeans.api.lexer.Language;
+import org.netbeans.junit.NbTestCase;
+import org.openide.cookies.EditorCookie;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.loaders.DataObject;
+
+/**
+ *
+ * @author lahvac
+ */
+public class UnusedDetectorTest extends NbTestCase {
+
+    public UnusedDetectorTest(String name) {
+        super(name);
+    }
+
+    @Test
+    public void testUnusedMethod() throws Exception {
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public class Test {\n" +
+                    "    private void unusedMethod() {}\n" +
+                    "}\n",
+                    "3:unusedMethod:NOT_USED");
+    }
+
+    @Test
+    public void testUnusedParameters() throws Exception {
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public abstract class Test {\n" +
+                    "    private void method(int unused) {}\n" +
+                    "    public void api(String notUnused1) {\n" +
+                    "        method(0);\n" +
+                    "        l(notUnused2 -> {});\n" +
+                    "        EffectivellyPrivate p = x -> x;\n" +
+                    "        nativeMethod(p.abstractMethod(0));\n" +
+                    "    }\n" +
+                    "    {\n" +
+                    "        l(notUnused3 -> {});\n" +
+                    "    }\n" +
+                    "    public String t1 = l(notUnused4 -> {});\n" +
+                    "    static {\n" +
+                    "        l(notUnused5 -> {});\n" +
+                    "    }\n" +
+                    "    public static String t2 = l(notUnused6 -> {});\n" +
+                    "    private native void nativeMethod(int notUnused7);\n" +
+                    "    private interface EffectivellyPrivate {\n" +
+                    "       int abstractMethod(int notUnused8);\n" +
+                    "    }\n" +
+                    "    private native brokenMethod(int notUnused9);\n" +
+                    "    public void l(I i) { }\n" +
+                    "    interface I {\n" +
+                    "        public String run(int i);\n" +
+                    "    }\n" +
+                    "}\n",
+                    "3:unused:NOT_READ",
+                    "22:<init>:NOT_USED");
+    }
+
+    @Test
+    public void testMethodRecursion() throws Exception {
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public class Test {\n" +
+                    "    private void unusedRecursive() {\n" +
+                    "        unusedRecursive();\n" +
+                    "        delay(Test::unusedRecursive);\n" +
+                    "        delay(() -> unusedRecursive());\n" +
+                    "    }\n" +
+                    "    private void used() {}\n" +
+                    "    public void use() { delay(Test::used); }\n" +
+                    "    public void delay(Runnable r) {}\n" +
+                    "}\n",
+                    "3:unusedRecursive:NOT_USED");
+    }
+
+    @Test
+    public void testForEachLoop() throws Exception {
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public class Test {\n" +
+                    "    public void test(String[] args) {\n" +
+                    "        for (String a : args) {\n" +
+                    "            System.err.println(a);\n" +
+                    "        }\n" +
+                    "    }\n" +
+                    "}\n");
+    }
+
+    @Test
+    public void testCompoundAssignment() throws Exception {
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public class Test {\n" +
+                    "    public int compound(int i) {\n" +
+                    "        int unused = 0;\n" +
+                    "        unused += 9;\n" +
+                    "        int used = 0;\n" +
+                    "        compound(used += 9);\n" +
+                    "        int unused2 = 0;\n" +
+                    "        int used2 = 0;\n" +
+                    "        unused2 += used2;\n" +
+                    "        int unused3 = 0;\n" +
+                    "        int u1 = unused3 += 4;\n" +
+                    "        int unused4 = 0;\n" +
+                    "        int u2 = 0;\n" +
+                    "        u2 = unused4 += 4;\n" +
+                    "        return u1 + u2;\n" +
+                    "    }\n" +
+                    "}\n",
+                    "4:unused:NOT_READ",
+                    "8:unused2:NOT_READ");
+    }
+
+    @Test
+    public void testBindingPatterns() throws Exception {
+        try {
+            SourceVersion.valueOf("RELEASE_14"); //NOI18N
+        } catch (IllegalArgumentException ex) {
+            //OK, no RELEASE_14, skip tests
+            return;
+        }
+        sourceLevel = "14";
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public class Test {\n" +
+                    "    public void compound(Object o1, Object o2) {\n" +
+                    "        if (o1 instanceof String s1) {\n" +
+                    "            return s1;\n" +
+                    "        }\n" +
+                    "        if (o2 instanceof String s2) {\n" +
+                    "            return null;\n" +
+                    "        }\n" +
+                    "        return null;\n" +
+                    "    }\n" +
+                    "}\n",
+                    "7:s2:NOT_READ");
+    }
+
+    @Test
+    public void testRecord() throws Exception {
+        try {
+            SourceVersion.valueOf("RELEASE_14"); //NOI18N
+        } catch (IllegalArgumentException ex) {
+            //OK, no RELEASE_14, skip tests
+            return;
+        }
+        sourceLevel = "14";
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public record Test(int i, long j) {\n" +
+                    "    private void unused() {}\n" +
+                    "}\n",
+                    "3:unused:NOT_USED");
+    }
+
+    @Test
+    public void testEnumConstructor() throws Exception {
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public enum E {\n" +
+                    "    A,\n" +
+                    "    B(1);\n" +
+                    "    E() {}\n" +
+                    "    E(int i) { System.err.println(s); }\n" +
+                    "    E(String s) { System.err.println(s); }\n" +
+                    "}\n",
+                    "7:<init>:NOT_USED");
+    }
+
+    @Test
+    public void testConstructor() throws Exception {
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public class Test {\n" +
+                    "    private Test() {}\n" +
+                    "    public static Test test() { return new Test(); }\n" +
+                    "}\n");
+    }
+
+    @Test
+    public void testCaught() throws Exception {
+        //Ignore unread caught exceptions. There are valid reasons why it
+        //could be unused; and there should be a separate hint checking if
+        //it makes sense to no use it:
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public class Test {\n" +
+                    "    public static Test test() {\n" +
+                    "        try {" +
+                    "            test();\n" +
+                    "        } catch (RuntimeException ex) {\n" +
+                    "        }\n" +
+                    "    }\n" +
+                    "}\n");
+    }
+
+    @Test
+    public void testPrivateConstructorUtilityClass1() throws Exception {
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public class Test {\n" +
+                    "    private Test() {}\n" +
+                    "    public static void test() {\n" +
+                    "    }\n" +
+                    "}\n");
+    }
+
+    @Test
+    public void testPrivateConstructorNotUtilityClass2() throws Exception {
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public class Test {\n" +
+                    "    private Test() {}\n" +
+                    "    public Test(int i) {}\n" +
+                    "    }\n" +
+                    "}\n",
+                    "3:<init>:NOT_USED");
+    }
+
+    @Test
+    public void testPrivateConstructorNotUtilityClass3() throws Exception {
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public abstract class Test {\n" +
+                    "    private Test() {}\n" +
+                    "    }\n" +
+                    "}\n",
+                    "3:<init>:NOT_USED");
+    }
+
+    @Test
+    public void testPrivateConstructorNotUtilityClass4() throws Exception {
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public class Test extends Exception {\n" +
+                    "    private Test() {}\n" +
+                    "    }\n" +
+                    "}\n",
+                    "3:<init>:NOT_USED");
+    }
+
+    @Test
+    public void testPrivateConstructorNotUtilityClass5() throws Exception {
+        performTest("test/Test.java",
+                    "package test;\n" +
+                    "public class Test implements I {\n" +
+                    "    private Test() {}\n" +
+                    "    }\n" +
+                    "}\n" +
+                    "interface I {}\n" +
+                    "}\n",
+                    "3:<init>:NOT_USED");
+    }
+
+    protected String sourceLevel = "1.8";
+
+    protected void performTest(String fileName, String code, String... expected) throws Exception {
+        SourceUtilsTestUtil.prepareTest(new String[] {}, new Object[] {new TestBase.MIMEResolverImpl()});
+
+	FileObject scratch = SourceUtilsTestUtil.makeScratchDir(this);
+	FileObject cache   = scratch.createFolder("cache");
+
+        File wd         = getWorkDir();
+        File testSource = new File(wd, "test/" + fileName + ".java");
+
+        testSource.getParentFile().mkdirs();
+        TestUtilities.copyStringToFile(testSource, code);
+
+        FileObject testSourceFO = FileUtil.toFileObject(testSource);
+
+        assertNotNull(testSourceFO);
+
+        if (sourceLevel != null) {
+            SourceUtilsTestUtil.setSourceLevel(testSourceFO, sourceLevel);
+        }
+
+        File testBuildTo = new File(wd, "test-build");
+
+        testBuildTo.mkdirs();
+
+        FileObject srcRoot = FileUtil.toFileObject(testSource.getParentFile());
+        SourceUtilsTestUtil.prepareTest(srcRoot,FileUtil.toFileObject(testBuildTo), cache);
+
+        final Document doc = getDocument(testSourceFO);
+
+        JavaSource source = JavaSource.forFileObject(testSourceFO);
+
+        assertNotNull(source);
+
+	final CountDownLatch l = new CountDownLatch(1);
+
+        source.runUserActionTask(new Task<CompilationController>() {
+            public void run(CompilationController parameter) {
+                try {
+                    parameter.toPhase(JavaSource.Phase.UP_TO_DATE);
+
+                    Set<String> result = UnusedDetector.findUnused(parameter)
+                                                       .stream()
+                                                       .map(ud -> parameter.getCompilationUnit().getLineMap().getLineNumber(parameter.getTrees().getSourcePositions().getStartPosition(ud.unusedElementPath.getCompilationUnit(), ud.unusedElementPath.getLeaf())) + ":" + ud.unusedElement.getSimpleName() + ":" + ud.reason.name())
+                                                       .collect(Collectors.toSet());
+                    assertEquals(new HashSet<>(Arrays.asList(expected)), result);
+                } catch (IOException e) {
+                    e.printStackTrace();
+                } finally {
+		    l.countDown();
+		}
+            }
+        }, true);
+
+        l.await();
+    }
+
+    private final Document getDocument(FileObject file) throws IOException {
+        DataObject od = DataObject.find(file);
+        EditorCookie ec = (EditorCookie) od.getCookie(EditorCookie.class);
+
+        if (ec != null) {
+            Document doc = ec.openDocument();
+
+            doc.putProperty(Language.class, JavaTokenId.language());
+            doc.putProperty("mimeType", "text/x-java");
+
+            return doc;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/java/java.hints/src/org/netbeans/modules/java/hints/bugs/Unused.java b/java/java.hints/src/org/netbeans/modules/java/hints/bugs/Unused.java
new file mode 100644
index 0000000..cc87f67
--- /dev/null
+++ b/java/java.hints/src/org/netbeans/modules/java/hints/bugs/Unused.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.netbeans.modules.java.hints.bugs;
+
+import com.sun.source.tree.Tree.Kind;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.lang.model.element.ElementKind;
+import org.netbeans.modules.java.editor.base.semantic.UnusedDetector;
+import org.netbeans.modules.java.editor.base.semantic.UnusedDetector.UnusedDescription;
+import org.netbeans.spi.editor.hints.ErrorDescription;
+import org.netbeans.spi.java.hints.ErrorDescriptionFactory;
+import org.netbeans.spi.java.hints.Hint;
+import org.netbeans.spi.java.hints.HintContext;
+import org.netbeans.spi.java.hints.TriggerTreeKind;
+import org.openide.util.NbBundle.Messages;
+
+/**
+ *
+ * @author lahvac
+ */
+@Hint(displayName = "#DN_org.netbeans.modules.java.hints.bugs.Unused", description = "#DESC_org.netbeans.modules.java.hints.bugs.Unused", category="bugs", options=Hint.Options.QUERY, suppressWarnings="unused")
+@Messages({
+    "DN_org.netbeans.modules.java.hints.bugs.Unused=Unused Element",
+    "DESC_org.netbeans.modules.java.hints.bugs.Unused=Detects and reports unused variables, methods and classes"
+})
+public class Unused {
+
+    @TriggerTreeKind(Kind.COMPILATION_UNIT)
+    public static List<ErrorDescription> unused(HintContext ctx) {
+         return UnusedDetector.findUnused(ctx.getInfo())
+                             .stream()
+                             .map(ud -> convertUnused(ctx, ud))
+                             .filter(err -> err != null)
+                             .collect(Collectors.toList());
+    }
+
+    @Messages({
+        "# {0} - variable name",
+        "ERR_NeitherReadOrWritten=Variable {0} is neither read or written to",
+        "# {0} - variable name",
+        "ERR_NotWritten=Variable {0} is never written to",
+        "# {0} - variable name",
+        "ERR_NotRead=Variable {0} is never read",
+        "# {0} - element name",
+        "ERR_NotUsed={0} is never used",
+        "ERR_NotUsedConstructor=Constructor is never used",
+    })
+    private static ErrorDescription convertUnused(HintContext ctx, UnusedDescription ud) {
+        //TODO: switch expression candidate!
+        String name = ud.unusedElement.getSimpleName().toString();
+        String message;
+        switch (ud.reason) {
+            case NOT_WRITTEN_READ: message = Bundle.ERR_NeitherReadOrWritten(name); break;
+            case NOT_WRITTEN: message = Bundle.ERR_NotWritten(name); break;
+            case NOT_READ: message = Bundle.ERR_NotRead(name); break;
+            case NOT_USED:
+                if (ud.unusedElement.getKind() == ElementKind.CONSTRUCTOR) {
+                    message = Bundle.ERR_NotUsedConstructor();
+                } else {
+                    message = Bundle.ERR_NotUsed(name);
+                }
+                break;
+            default:
+                throw new IllegalStateException("Unknown unused type: " + ud.reason);
+        }
+        return ErrorDescriptionFactory.forName(ctx, ud.unusedElementPath, message);
+    }
+}
diff --git a/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/bugs/UnusedTest.java b/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/bugs/UnusedTest.java
new file mode 100644
index 0000000..2b41dba
--- /dev/null
+++ b/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/bugs/UnusedTest.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.netbeans.modules.java.hints.bugs;
+
+import org.netbeans.junit.NbTestCase;
+import org.netbeans.modules.java.hints.test.api.HintTest;
+
+/**
+ *
+ * @author lahvac
+ */
+public class UnusedTest extends NbTestCase {
+
+    public UnusedTest(String name) {
+        super(name);
+    }
+
+    public void testUnused() throws Exception {
+        HintTest
+                .create()
+                .input("package test;\n" +
+                       "public class Test {\n" +
+                       "    private class UnusedClass {}\n" +
+                       "    private void unusedMethod() {}\n" +
+                       "    private int unusedField;\n" +
+                       "    private void test(int unusedParam) {}\n" +
+                       "    public void test2() {test(1);}\n" +
+                       "    private Test() {}\n" +
+                       "}\n")
+                .run(Unused.class)
+                .assertWarnings("2:18-2:29:verifier:" + Bundle.ERR_NotUsed("UnusedClass"),
+                                "3:17-3:29:verifier:" + Bundle.ERR_NotUsed("unusedMethod"),
+                                "4:16-4:27:verifier:" + Bundle.ERR_NotRead("unusedField"),
+                                "5:26-5:37:verifier:" + Bundle.ERR_NotRead("unusedParam"),
+                                "7:12-7:16:verifier:" + Bundle.ERR_NotUsedConstructor());
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@netbeans.apache.org
For additional commands, e-mail: commits-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists