From aed68f3c5f1cef6e8ca96b841bc51bd3627b6cc2 Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Sat, 1 Dec 2018 08:03:19 +0100 Subject: [PATCH] feature: NameScope detects conflicts on names --- .../java/spoon/reflect/visitor/NameScope.java | 219 ++++++++++++++++++ .../reflect/visitor/NameScopeOfType.java | 185 +++++++++++++++ .../reflect/visitor/SimpleNameScope.java | 64 +++++ .../imports/name_scope/NameScopeTest.java | 124 ++++++++++ .../name_scope/testclasses/Renata.java | 34 +++ 5 files changed, 626 insertions(+) create mode 100644 src/main/java/spoon/reflect/visitor/NameScope.java create mode 100644 src/main/java/spoon/reflect/visitor/NameScopeOfType.java create mode 100644 src/main/java/spoon/reflect/visitor/SimpleNameScope.java create mode 100644 src/test/java/spoon/test/imports/name_scope/NameScopeTest.java create mode 100644 src/test/java/spoon/test/imports/name_scope/testclasses/Renata.java diff --git a/src/main/java/spoon/reflect/visitor/NameScope.java b/src/main/java/spoon/reflect/visitor/NameScope.java new file mode 100644 index 00000000000..ffb9b24a122 --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/NameScope.java @@ -0,0 +1,219 @@ +/** + * Copyright (C) 2006-2018 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.reflect.visitor; + +import java.lang.annotation.Annotation; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.function.Function; + +import spoon.SpoonException; +import spoon.reflect.code.CtBlock; +import spoon.reflect.code.CtCatch; +import spoon.reflect.code.CtLambda; +import spoon.reflect.code.CtLocalVariable; +import spoon.reflect.declaration.CtAnnotationType; +import spoon.reflect.declaration.CtClass; +import spoon.reflect.declaration.CtCompilationUnit; +import spoon.reflect.declaration.CtConstructor; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtEnum; +import spoon.reflect.declaration.CtInterface; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.declaration.CtNamedElement; +import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.CtTypeMember; +import spoon.support.Experimental; + +/** + * Maps names to CtElements, which are visible at current scanning place + */ +@Experimental +public abstract class NameScope { + + private static NameScope EMPTY = new NameScope(null, null) { + @Override + protected T forEachLocalElementByName(String name, Function consumer) { + return null; + } + }; + + /** + * The {@link EarlyTerminatingScanner} implementation, which knows all visible elements in the scope + * @param + */ + public static class Scanner extends EarlyTerminatingScanner { + private final Deque scopes = new ArrayDeque<>(); + protected void enter(spoon.reflect.declaration.CtElement e) { + NameScope newFinder = NameScope.onElement(scopes.peek(), e); + if (newFinder != null) { + scopes.push(newFinder); + } + } + protected void exit(spoon.reflect.declaration.CtElement e) { + NameScope topFinder = scopes.peek(); + if (topFinder != null && topFinder.getScopeElement() == e) { + //we are living scope of this ConflictFinder. Pop it + scopes.pop(); + } + } + public NameScope getNameScope() { + NameScope ns = scopes.peek(); + return ns == null ? EMPTY : ns; + } + public Deque getScopes() { + return scopes; + } + } + + /** + * Call it for each visited CtElement + * @param parent the parent ConflictFinder + * @param target an element + * @return new {@link NameScope} if `target` element declares new naming scope or null if there is no new scope + */ + protected static NameScope onElement(NameScope parent, CtElement target) { + class Visitor extends CtAbstractVisitor { + NameScope finder = null; + @Override + public void visitCtCompilationUnit(CtCompilationUnit compilationUnit) { + //compilation unit items are added in NameScopeOfType, because they depend on the inhertance hierarchy of the type itself + } + @Override + public void visitCtClass(CtClass ctClass) { + finder = new NameScopeOfType(parent, ctClass); + } + @Override + public void visitCtInterface(CtInterface intrface) { + finder = new NameScopeOfType(parent, intrface); + } + @Override + public > void visitCtEnum(CtEnum ctEnum) { + finder = new NameScopeOfType(parent, ctEnum); + } + @Override + public void visitCtAnnotationType(CtAnnotationType annotationType) { + finder = new NameScopeOfType(parent, annotationType); + } + @Override + public void visitCtMethod(CtMethod m) { + finder = new SimpleNameScope(parent, m, m.getParameters()); + } + @Override + public void visitCtConstructor(CtConstructor c) { + finder = new SimpleNameScope(parent, c, c.getParameters()); + } + @Override + public void visitCtLambda(CtLambda lambda) { + finder = new SimpleNameScope(parent, lambda, lambda.getParameters()); + } + @Override + public void visitCtCatch(CtCatch catchBlock) { + finder = new SimpleNameScope(parent, catchBlock).addVariable(catchBlock.getParameter()); + } + @Override + public void visitCtBlock(CtBlock block) { + finder = new SimpleNameScope(parent, block); + } + @Override + public void visitCtLocalVariable(CtLocalVariable localVariable) { + if (parent instanceof SimpleNameScope) { + ((SimpleNameScope) parent).addVariable(localVariable); + } else { + throw new SpoonException("Cannot add local variable when parent is missing"); + } + } + }; + Visitor scanner = new Visitor(); + target.accept(scanner); + return scanner.finder; + } + + private final NameScope parent; + private final CtElement scopeElement; + + protected NameScope(NameScope parent, CtElement scopeElement) { + this.parent = parent; + this.scopeElement = scopeElement; + } + + /** + * @return the {@link CtElement} which represents the current scope + */ + public final CtElement getScopeElement() { + return scopeElement; + } + + /** + * @param name to be searched simple name + * @param consumer is called for each named element with same name which are accessible from this {@link NameScope} + * as long as there are some elements and consumer returns null. If `consumer` return not null value then it is returned + * @return the value returned by `consumer` or null + */ + public final T forEachElementByName(String name, Function consumer) { + T r = forEachLocalElementByName(name, consumer); + if (r != null) { + return r; + } + if (scopeElement instanceof CtNamedElement) { + CtNamedElement named = (CtNamedElement) scopeElement; + if (name.equals(named.getSimpleName())) { + r = consumer.apply(named); + if (r != null) { + return r; + } + } + } + if (parent != null) { + return parent.forEachElementByName(name, consumer); + } + return null; + } + + protected abstract T forEachLocalElementByName(String name, Function consumer); + + protected static T forEachByName(Map map, String name, Function consumer) { + CtNamedElement named = map.get(name); + if (named != null) { + return consumer.apply(named); + } + return null; + } + + /** + * @return true if two methods can come from same static import + * + * For example `import static org.junit.Assert.assertEquals;` + * imports methods with signatures: + * assertEquals(Object, Object) + * assertEquals(Object[], Object[]) + * assertEquals(long, long) + * ... + */ + static boolean isSameStaticImport(CtNamedElement m1, CtNamedElement m2) { + if (m1 instanceof CtTypeMember && m2 instanceof CtTypeMember) { + if (m1.getSimpleName().equals(m2.getSimpleName())) { + CtType declType1 = ((CtTypeMember) m1).getDeclaringType(); + CtType declType2 = ((CtTypeMember) m2).getDeclaringType(); + //may be we should check isSubTypeOf instead + return declType1 == declType2; + } + } + return false; + } +} diff --git a/src/main/java/spoon/reflect/visitor/NameScopeOfType.java b/src/main/java/spoon/reflect/visitor/NameScopeOfType.java new file mode 100644 index 00000000000..b968fba044b --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/NameScopeOfType.java @@ -0,0 +1,185 @@ +/** + * Copyright (C) 2006-2018 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.reflect.visitor; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import spoon.reflect.declaration.CtCompilationUnit; +import spoon.reflect.declaration.CtField; +import spoon.reflect.declaration.CtImport; +import spoon.reflect.declaration.CtImportKind; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.declaration.CtNamedElement; +import spoon.reflect.declaration.CtPackage; +import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.CtTypeMember; +import spoon.reflect.reference.CtExecutableReference; +import spoon.reflect.reference.CtFieldReference; +import spoon.reflect.reference.CtPackageReference; +import spoon.reflect.reference.CtTypeMemberWildcardImportReference; +import spoon.reflect.reference.CtTypeReference; +import spoon.reflect.visitor.filter.AllTypeMembersFunction; + +/** + * Represents NameScope of Type. Knows all accessible fields, nested type names and method names + */ +class NameScopeOfType extends NameScope { + private Map fieldsByName; + private Map typesByName; + private Map methodsByName; + + NameScopeOfType(NameScope conflictFinder, CtType p_type) { + super(conflictFinder, p_type); + } + + @Override + protected T forEachLocalElementByName(String name, Function consumer) { + assureCacheInitialized(); + T r = forEachByName(fieldsByName, name, consumer); + if (r != null) { + return r; + } + r = forEachByName(typesByName, name, consumer); + if (r != null) { + return r; + } + r = forEachByName(methodsByName, name, consumer); + if (r != null) { + return r; + } + return null; + } + + private void assureCacheInitialized() { + if (fieldsByName == null) { + //collect names of type members which are visible in this type + fieldsByName = new HashMap<>(); + typesByName = new HashMap<>(); + methodsByName = new HashMap<>(); + CtType type = (CtType) getScopeElement(); + type.map(new AllTypeMembersFunction().setMode(AllTypeMembersFunction.Mode.SKIP_PRIVATE)).forEach((CtTypeMember typeMember) -> { + //the local members are visited first. Then members of super types/interfaces + if (typeMember instanceof CtField) { + putIfNotExists(fieldsByName, (CtField) typeMember); + } else if (typeMember instanceof CtType) { + putIfNotExists(typesByName, (CtType) typeMember); + } else if (typeMember instanceof CtMethod) { + putIfNotExists(methodsByName, (CtMethod) typeMember); + } + }); + if (type.isTopLevel()) { + CtCompilationUnit cu = type.getPosition().getCompilationUnit(); + if (cu != null) { + //add types and static fields and methods from compilation unit + addCompilationUnitNames(cu); + } + } + } + } + + /* + * sort wildcard imports as last. The wildcard import of type has lower priority then explicit import of type + */ + private static final Comparator importComparator = new Comparator() { + @Override + public int compare(CtImport o1, CtImport o2) { + CtImportKind k1 = o1.getImportKind(); + CtImportKind k2 = o2.getImportKind(); + return getOrderOfImportKind(k1) - getOrderOfImportKind(k2); + } + + private int getOrderOfImportKind(CtImportKind ik) { + switch (ik) { + case ALL_STATIC_MEMBERS: + return 2; + case ALL_TYPES: + return 1; + default: + return 0; + } + } + }; + + private void addCompilationUnitNames(CtCompilationUnit compilationUnit) { + CtType type = (CtType) getScopeElement(); + CtTypeReference typeRef = type.getReference(); + //all imported types and static members are visible too + compilationUnit.getImports().stream().sorted(importComparator).forEach(aImport -> { + aImport.accept(new CtImportVisitor() { + @Override + public void visitTypeImport(CtTypeReference typeReference) { + putIfNotExists(typesByName, typeReference.getTypeDeclaration()); + } + @Override + public void visitMethodImport(CtExecutableReference executableReference) { + putIfNotExists(methodsByName, executableReference.getExecutableDeclaration()); + } + @Override + public void visitFieldImport(CtFieldReference fieldReference) { + putIfNotExists(fieldsByName, fieldReference.getFieldDeclaration()); + } + @Override + public void visitAllTypesImport(CtPackageReference packageReference) { + CtPackage pack = packageReference.getDeclaration(); + if (pack != null) { + for (CtType type : pack.getTypes()) { + //add only types which are not yet imported. Explicit import wins over wildcard import + putIfNotExists(typesByName, type); + } + } + } + @Override + public void visitAllStaticMembersImport(CtTypeMemberWildcardImportReference typeReference) { + CtType type = typeReference.getDeclaration(); + type.map(new AllTypeMembersFunction().setMode(AllTypeMembersFunction.Mode.SKIP_PRIVATE)).forEach((CtTypeMember typeMember) -> { + if (typeMember.isStatic() && typeRef.canAccess(typeMember)) { + if (typeMember instanceof CtField) { + putIfNotExists(fieldsByName, typeMember); + } else if (typeMember instanceof CtMethod) { + putIfNotExists(methodsByName, typeMember); + } + } + }); + } + }); + }); + //names of all types of same package are visible too, but with lower priority then explicitly imported elements + CtPackage pack = compilationUnit.getDeclaredPackage(); + if (pack != null) { + for (CtType packageType : pack.getTypes()) { + if (!typesByName.containsKey(packageType.getSimpleName())) { + typesByName.put(packageType.getSimpleName(), packageType); + } + } + } + } + + //assures that type members nearer to local type are used + private void putIfNotExists(Map map, T element) { + if (element == null) { + //noclasspath mode. Ignore that. + return; + } + String name = element.getSimpleName(); + if (!map.containsKey(name)) { + map.put(name, element); + } + } +} diff --git a/src/main/java/spoon/reflect/visitor/SimpleNameScope.java b/src/main/java/spoon/reflect/visitor/SimpleNameScope.java new file mode 100644 index 00000000000..b66db2b60bb --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/SimpleNameScope.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2006-2018 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.reflect.visitor; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtNamedElement; +import spoon.reflect.declaration.CtParameter; +import spoon.reflect.declaration.CtVariable; + +/** + * {@link NameScope} for exact set of named elements. There can be: + *
    + *
  • parameter names of constructor, method or lambda + *
  • parameter name of catch variable + *
  • local variables of block + *
+ */ +class SimpleNameScope extends NameScope { + + private final Map elementsByName; + + SimpleNameScope(NameScope parent, CtElement scopeElement, List> parameters) { + super(parent, scopeElement); + elementsByName = new HashMap<>(parameters.size()); + for (CtParameter parameter : parameters) { + elementsByName.put(parameter.getSimpleName(), parameter); + } + } + + SimpleNameScope(NameScope parent, CtElement scopeElement) { + super(parent, scopeElement); + elementsByName = new HashMap<>(); + } + + @Override + protected T forEachLocalElementByName(String name, Function consumer) { + return forEachByName(elementsByName, name, consumer); + } + + SimpleNameScope addVariable(CtVariable var) { + //do not check conflict here. It is OK that local variable hides parameter + elementsByName.put(var.getSimpleName(), var); + return this; + } +} diff --git a/src/test/java/spoon/test/imports/name_scope/NameScopeTest.java b/src/test/java/spoon/test/imports/name_scope/NameScopeTest.java new file mode 100644 index 00000000000..70bf3bf541c --- /dev/null +++ b/src/test/java/spoon/test/imports/name_scope/NameScopeTest.java @@ -0,0 +1,124 @@ +/** + * Copyright (C) 2006-2018 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.test.imports.name_scope; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +import java.io.File; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +import spoon.reflect.code.CtLiteral; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtField; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.declaration.CtType; +import spoon.reflect.path.CtRole; +import spoon.reflect.visitor.NameScope; +import spoon.reflect.visitor.chain.CtScannerListener; +import spoon.reflect.visitor.chain.ScanningMode; +import spoon.test.imports.name_scope.testclasses.Renata; +import spoon.testing.utils.ModelUtils; + +public class NameScopeTest { + + @Test + public void testNameScopeScanner() throws Exception { + //contract: check that NameScope knows expected name. + CtType typeRenata = ModelUtils.buildClass(launcher -> { + //needed to compute imports + launcher.getEnvironment().setAutoImports(true); + }, Renata.class); + + CtField fieldMichal = typeRenata.getField("michal"); + CtType typeTereza = typeRenata.getNestedType("Tereza"); + + CtMethod methodDraw = typeTereza.getMethodsByName("draw").get(0); + + CtType typeFile = typeRenata.getFactory().Type().createReference(File.class).getTypeDeclaration(); + CtType typeSystem = typeRenata.getFactory().Type().createReference(System.class).getTypeDeclaration(); + CtMethod methodCurrentTimeMillis = typeSystem.getMethodsByName("currentTimeMillis").get(0); + + CtType typeFiles = typeRenata.getFactory().Type().createReference(Files.class).getTypeDeclaration(); + CtMethod methodsNewDirectoryStream = typeFiles.getMethodsByName("newDirectoryStream").get(0); + + NameScope.Scanner scanner = new NameScope.Scanner<>(); + scanner.setVisitCompilationUnitContent(true); + scanner.setListener(new CtScannerListener() { + @Override + public ScanningMode enter(CtRole role, CtElement element) { + if (element instanceof CtLiteral) { + CtLiteral literal = (CtLiteral) element; + //check that NameScope is aware of all names, which are visible at position of the literals + if ("1".equals(literal.getValue())) { + //contract: the local variables are visible after they are declared + assertNameScope(Arrays.asList(), scanner.getNameScope(), "count"); + assertNameScope(Arrays.asList("String theme"), scanner.getNameScope(), "theme"); + assertNameScope(Arrays.asList(methodDraw), scanner.getNameScope(), "draw"); + assertNameScope(Arrays.asList(typeTereza), scanner.getNameScope(), "Tereza"); + assertNameScope(Arrays.asList(fieldMichal), scanner.getNameScope(), "michal"); + assertNameScope(Arrays.asList(typeRenata), scanner.getNameScope(), "Renata"); + //contract: imported types are visible too + assertNameScope(Arrays.asList(typeFile), scanner.getNameScope(), "File"); + //contract: imported static methods are visible too + assertNameScope(Arrays.asList(methodCurrentTimeMillis), scanner.getNameScope(), "currentTimeMillis"); + //contract: type members imported by wildcard are visible too + assertNameScope(Arrays.asList(methodsNewDirectoryStream), scanner.getNameScope(), "newDirectoryStream"); + //contract: The names are case sensitive + assertNameScope(Arrays.asList(), scanner.getNameScope(), "Michal"); + //the names which are not visible, must not be returned + assertNameScope(Arrays.asList(), scanner.getNameScope(), "void"); + assertNameScope(Arrays.asList(), scanner.getNameScope(), "String"); + //type members of System are not visible + assertNameScope(Arrays.asList(), scanner.getNameScope(), "setIn"); + //type member itself whose field is imported is not visible + assertNameScope(Arrays.asList(), scanner.getNameScope(), "System"); + //type member itself whose type members are imported by wildcard are not visible + assertNameScope(Arrays.asList(), scanner.getNameScope(), "Fields"); + } else if ("2".equals(literal.getValue())) { + //contract: the local variables are visible after they are declared + assertNameScope(Arrays.asList("int count"), scanner.getNameScope(), "count"); + } + } + return ScanningMode.NORMAL; + } + }); + scanner.scan(typeRenata.getPosition().getCompilationUnit()); + } + + private void assertNameScope(List expectedElements, NameScope nameScope, String name) { + List realElements = new ArrayList<>(); + nameScope.forEachElementByName(name, e -> realElements.add(e)); + assertEquals(expectedElements.size(), realElements.size()); + for (int i = 0; i < expectedElements.size(); i++) { + Object expected = expectedElements.get(i); + assertNotNull(expected); + if (expected instanceof String) { + String expectedString = (String) expected; + assertEquals(expectedString, realElements.get(i).toString()); + } else { + assertSame(expected, realElements.get(i)); + } + } + } +} diff --git a/src/test/java/spoon/test/imports/name_scope/testclasses/Renata.java b/src/test/java/spoon/test/imports/name_scope/testclasses/Renata.java new file mode 100644 index 00000000000..6043f3acd75 --- /dev/null +++ b/src/test/java/spoon/test/imports/name_scope/testclasses/Renata.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2006-2018 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.test.imports.name_scope.testclasses; + +import java.io.File; +import static java.lang.System.currentTimeMillis; +import static java.nio.file.Files.*; + +public class Renata { + + String michal; + + class Tereza { + void draw(String theme) { + System.out.println("1"); + int count; + System.out.println("2"); + } + } +}