Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* 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.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.TreePath;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
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.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.util.ElementFilter;
import org.netbeans.api.java.source.CompilationInfo;
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;
import org.openide.util.NbCollections;

@Hint(displayName="Magic Constant", description="Verifies magic constants", category="bugs")
public class MagicConstantHint {

@TriggerTreeKind({Kind.METHOD_INVOCATION, Kind.NEW_CLASS})
@Messages({
"# {0} - the list of valid values",
"ERR_NotAValidValue=Not a valid value, expected one of: {0}"
})
public static List<ErrorDescription> hint(HintContext ctx) {
Element el = ctx.getInfo().getTrees().getElement(ctx.getPath());

if (el == null || (el.getKind() != ElementKind.METHOD && el.getKind() != ElementKind.CONSTRUCTOR)) return null;

List<ErrorDescription> result = new ArrayList<ErrorDescription>();
List<? extends ExpressionTree> arguments = ctx.getPath().getLeaf().getKind() == Kind.METHOD_INVOCATION ?
((MethodInvocationTree) ctx.getPath().getLeaf()).getArguments() :
((NewClassTree) ctx.getPath().getLeaf()).getArguments();
int parameterIndex = 0;

for (VariableElement param : ((ExecutableElement) el).getParameters()) {
int currentParam = parameterIndex++;

List<? extends AnnotationMirror> annotations = ctx.getInfo().getElementUtilities().getAugmentedAnnotationMirrors(param);
List<VariableElement> validValues = new ArrayList<VariableElement>();

for (AnnotationMirror am : annotations) {
if (!((TypeElement) am.getAnnotationType().asElement()).getQualifiedName().contentEquals("org.intellij.lang.annotations.MagicConstant")) continue;

//TODO: caching(!)
for (Entry<? extends ExecutableElement, ? extends AnnotationValue> e : am.getElementValues().entrySet()) {
boolean isFlagsFromClass = e.getKey().getSimpleName().contentEquals("flagsFromClass");
boolean isValuesFromClass = e.getKey().getSimpleName().contentEquals("valuesFromClass");
if ((isFlagsFromClass || isValuesFromClass) && e.getValue().getValue() instanceof DeclaredType) {
for (VariableElement flag : ElementFilter.fieldsIn(((TypeElement) ((DeclaredType) e.getValue().getValue()).asElement()).getEnclosedElements())) {
if (!flag.getModifiers().contains(Modifier.STATIC)) continue;

validValues.add(flag);
}

break;
}
if (e.getKey().getSimpleName().contentEquals("intValues") && e.getValue().getValue() instanceof Collection) {
for (AnnotationValue field : NbCollections.iterable(NbCollections.checkedIteratorByFilter(((Collection) e.getValue().getValue()).iterator(), AnnotationValue.class, false))) {
if (!(field.getValue() instanceof String)) continue;
Element foundField = lookupField(ctx.getInfo(), (String) field.getValue());
if (foundField == null || foundField.getKind() != ElementKind.FIELD) continue;

validValues.add((VariableElement) foundField);
}

break;
}
}
}

if (validValues.isEmpty()) continue;

Element resolved = ctx.getInfo().getTrees().getElement(new TreePath(ctx.getPath(), arguments.get(currentParam)));

//TODO: "values" vs. "flags":
if (!validValues.contains(resolved)) {
result.add(ErrorDescriptionFactory.forTree(ctx, arguments.get(currentParam), Bundle.ERR_NotAValidValue(validValues.stream().map(ve -> ctx.getInfo().getElementUtilities().getElementName(ve, false)).collect(Collectors.joining(", ")))));
}
}

return result;
}

//should be replaced with ElementUtilities.findElement when the platform is NB7.4+:
private static Element lookupField(CompilationInfo info, String field) {
int lastDot = field.lastIndexOf('.');
if (lastDot == (-1)) return null;
TypeElement clazz = info.getElements().getTypeElement(field.substring(0, lastDot));
if (clazz == null) return null;
String simpleName = field.substring(lastDot + 1);
for (VariableElement var : ElementFilter.fieldsIn(clazz.getEnclosedElements())) {
if (var.getSimpleName().contentEquals(simpleName))
return var;
}
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -540,14 +540,10 @@ private static State getStateFromAnnotations(CompilationInfo info, Element e) {
return getStateFromAnnotations(info, e, State.POSSIBLE_NULL);
}

private static final AnnotationMirrorGetter OVERRIDE_ANNOTATIONS = Lookup.getDefault().lookup(AnnotationMirrorGetter.class);

private static State getStateFromAnnotations(CompilationInfo info, Element e, State def) {
if (e == null) return def;

Iterable<? extends AnnotationMirror> mirrors = OVERRIDE_ANNOTATIONS != null ? OVERRIDE_ANNOTATIONS.getAnnotationMirrors(info, e) : null;

if (mirrors == null) mirrors = e.getAnnotationMirrors();
Iterable<? extends AnnotationMirror> mirrors = info.getElementUtilities().getAugmentedAnnotationMirrors(e);

for (AnnotationMirror am : mirrors) {
String simpleName = ((TypeElement) am.getAnnotationType().asElement()).getSimpleName().toString();
Expand All @@ -568,10 +564,6 @@ private static State getStateFromAnnotations(CompilationInfo info, Element e, St
return def;
}

public interface AnnotationMirrorGetter {
public Iterable<? extends AnnotationMirror> getAnnotationMirrors(CompilationInfo info, Element el);
}

private static final class VisitorImpl extends CancellableTreeScanner<State, Void> {

private final HintContext ctx;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* 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 java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.stream.Collectors;
import org.junit.Test;
import org.netbeans.modules.java.hints.test.api.HintTest;
import org.netbeans.modules.java.source.annotations.AugmentedAnnotations;

public class MagicConstantHintTest {

@Test
public void testFlagsFromClass() throws Exception {
writeAnnotations("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<root>\n" +
"<item name=\"test.Test void test(int) 0\">" +
"<annotation name=\"org.intellij.lang.annotations.MagicConstant\">" +
"<val name=\"flagsFromClass\" val=\"java.lang.reflect.Modifier.class\" /> " +
"</annotation>\n" +
"</item>\n" +
"</root>\n");

String modifiers =
Arrays.stream(Modifier.class.getDeclaredFields())
.filter(f -> (f.getModifiers() & Modifier.STATIC) != 0)
.map(f -> f.getName())
.collect(Collectors.joining(", "));

HintTest.create()
.input("package test;\n" +
"public class Test {\n" +
" public void test(int i) {\n" +
" test(java.awt.event.InputEvent.META_MASK);\n" +
" }\n" +
"}\n")
.run(MagicConstantHint.class)
.assertWarnings("3:13-3:48:verifier:" + Bundle.ERR_NotAValidValue(modifiers));

HintTest.create()
.input("package test;\n" +
"public class Test {\n" +
" public void test(int i) {\n" +
" test(java.lang.reflect.Modifier.PUBLIC);\n" +
" }\n" +
"}\n")
.run(MagicConstantHint.class)
.assertWarnings();
}

@Test
public void testIntValues() throws Exception {
writeAnnotations("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<root>\n" +
"<item name=\"test.Test void test(int) 0\">" +
"<annotation name=\"org.intellij.lang.annotations.MagicConstant\">" +
"<val name=\"intValues\" val=\"{java.lang.reflect.Modifier.PUBLIC}\" /> " +
"</annotation>\n" +
"</item>\n" +
"</root>\n");

HintTest.create()
.input("package test;\n" +
"public class Test {\n" +
" public void test(int i) {\n" +
" test(java.lang.reflect.Modifier.PRIVATE);\n" +
" }\n" +
"}\n")
.run(MagicConstantHint.class)
.assertWarnings("3:13-3:47:verifier:" + Bundle.ERR_NotAValidValue("PUBLIC"));

HintTest.create()
.input("package test;\n" +
"public class Test {\n" +
" public void test(int i) {\n" +
" test(java.lang.reflect.Modifier.PUBLIC);\n" +
" }\n" +
"}\n")
.run(MagicConstantHint.class)
.assertWarnings();
}

@Test
public void testConstructorAndValuesFromClass() throws Exception {
writeAnnotations("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<root>\n" +
"<item name=\"test.Test Test(int) 0\">" +
"<annotation name=\"org.intellij.lang.annotations.MagicConstant\">" +
"<val name=\"valuesFromClass\" val=\"test.Values.class\" /> " +
"</annotation>\n" +
"</item>\n" +
"</root>\n");

HintTest.create()
.input("package test;\n" +
"public class Test {\n" +
" public static final int V = 1;\n" +
" public Test(int i) {\n" +
" new Test(V);\n" +
" }\n" +
"}\n" +
"class Values {\n" +
" public static final int V = 0;\n" +
"}\n")
.run(MagicConstantHint.class)
.assertWarnings("4:17-4:18:verifier:" + Bundle.ERR_NotAValidValue("V"));

HintTest.create()
.input("package test;\n" +
"public class Test {\n" +
" public Test(int i) {\n" +
" new Test(Values.V);\n" +
" }\n" +
"}\n" +
"class Values {\n" +
" public static final int V = 0;\n" +
"}\n")
.run(MagicConstantHint.class)
.assertWarnings();
}

private static void writeAnnotations(String content) throws IOException {
AugmentedAnnotations.setAugmentedAnnotationsForTests(content);
}

}
6 changes: 6 additions & 0 deletions java/java.kit/nbproject/project.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
<specification-version>1.0</specification-version>
</run-dependency>
</dependency>
<dependency>
<code-name-base>org.netbeans.libs.jdkannotations</code-name-base>
<run-dependency>
<specification-version>1.0</specification-version>
</run-dependency>
</dependency>
<dependency>
<code-name-base>org.netbeans.modules.ant.kit</code-name-base>
<run-dependency>
Expand Down
2 changes: 1 addition & 1 deletion java/java.source.base/nbproject/project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ javadoc.name=Java Source Base
javadoc.title=Java Source Base
javadoc.arch=${basedir}/arch.xml
javadoc.apichanges=${basedir}/apichanges.xml
spec.version.base=2.80.0
spec.version.base=2.81.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:\
Expand Down
8 changes: 8 additions & 0 deletions java/java.source.base/nbproject/project.xml
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,14 @@
<specification-version>8.0</specification-version>
</run-dependency>
</dependency>
<dependency>
<code-name-base>org.openide.util.ui</code-name-base>
<build-prerequisite/>
<compile-dependency/>
<run-dependency>
<specification-version>9.39</specification-version>
</run-dependency>
</dependency>
</module-dependencies>
<test-dependencies>
<test-type>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
Expand All @@ -90,6 +91,7 @@
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.modules.java.source.annotations.AugmentedAnnotations;
import org.netbeans.modules.java.source.builder.ElementsService;
import org.netbeans.modules.java.source.base.SourceLevelUtils;
import org.openide.util.Pair;
Expand Down Expand Up @@ -1131,6 +1133,20 @@ private ExecutableElement recordCanonicalConstructor(TypeElement recordType) {
return exported;
}

/**
* Get any annotations which with the given {@code Element} has been annotated,
* as if by calling {@link Element#getAnnotationMirrors()}, and add any externally
* defined annotations for the {@code Element}.
*
* @param e the {@code Element} for which the annotations should be retrieved
* @return the annotations with which the given {@code Element} has been
* annotated and any externally attached annotations.
* @since 2.81
*/
public List<? extends AnnotationMirror> getAugmentedAnnotationMirrors(Element e) {
return AugmentedAnnotations.getAugmentedAnnotationMirrors(info, e);
}

// private implementation --------------------------------------------------

private static final Set<Modifier> NOT_OVERRIDABLE = EnumSet.of(Modifier.STATIC, Modifier.FINAL, Modifier.PRIVATE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
import org.netbeans.modules.java.source.ElementUtils;
import org.netbeans.modules.java.source.JavadocHelper;
import org.netbeans.modules.java.source.ModuleNames;
import org.netbeans.modules.java.source.annotations.AugmentedAnnotations;
import org.netbeans.modules.java.source.indexing.FQN2Files;
import org.netbeans.modules.java.source.indexing.JavaCustomIndexer;
import org.netbeans.modules.java.source.parsing.ClassParser;
Expand Down Expand Up @@ -1531,4 +1532,8 @@ public static String classNameFor(ClasspathInfo info, String relativePath, Neste
return className;
}
}

public static boolean attachAnnotation(CompilationInfo info, Element e, String annotationFQN) {
return AugmentedAnnotations.attachAnnotation(info, e, annotationFQN);
}
}
Loading
Loading