Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: introduce the concept of lexical scope (interface LexicalScope) #2813

Merged
merged 7 commits into from
Jan 23, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 4 additions & 5 deletions src/main/java/spoon/reflect/visitor/AbstractNameScope.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@
/**
* Maps names to CtElements, which are visible at current scanning place
*/
abstract class AbstractNameScope implements NameScope {
abstract class AbstractNameScope implements LexicalScope {

private final NameScope parent;
private final LexicalScope parent;
private final CtElement scopeElement;

protected AbstractNameScope(NameScope parent, CtElement scopeElement) {
protected AbstractNameScope(LexicalScope parent, CtElement scopeElement) {
this.parent = parent;
this.scopeElement = scopeElement;
}
Expand All @@ -44,8 +44,7 @@ public final CtElement getScopeElement() {
return scopeElement;
}

@Override
public final Optional<NameScope> getParent() {
public final Optional<LexicalScope> getParent() {
return Optional.ofNullable(parent);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,27 @@
*/
package spoon.reflect.visitor;

import java.util.Optional;
import java.util.function.Function;

import spoon.reflect.declaration.CtElement;
import spoon.reflect.declaration.CtNamedElement;
import spoon.support.Experimental;

/**
* Maps simple names to {@link CtElement}, which represents that simple name in current scope
* Represents that a lexical scope in the language
*
* See https://en.wikipedia.org/wiki/Scope_(computer_science)#Lexical_scoping
*/
@Experimental
public interface NameScope {
public interface LexicalScope {
/**
* @return the {@link CtElement} which represents the current scope
*/
CtElement getScopeElement();

/**
* @return outer scope
*/
Optional<NameScope> getParent();

/**
* @param name to be searched simple name
* @param fnc is called for each named element with same simple name, which is defined in this or parent {@link NameScope}.
* @param fnc is called for each named element with same simple name, which is defined in this or parent {@link LexicalScope}.
* Function `fnc` is called as long as there are some matching elements and `fnc` returns null.
* If `fnc` returns not null value then searching is stopped and that value is a returned
* @return the value returned by `fnc` or null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import java.lang.annotation.Annotation;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.function.Function;

import spoon.SpoonException;
Expand All @@ -21,21 +24,21 @@
import spoon.reflect.declaration.CtNamedElement;

/**
* The {@link EarlyTerminatingScanner} implementation,
* which knows mapping of simple names to elements in the actually scanned scope.
* Responsible for building lexical scopes.
*
* @param <T> the type of the result produced by this scanner.
*/
public class NameScopeScanner<T> extends EarlyTerminatingScanner<T> {
private final Deque<NameScope> scopes = new ArrayDeque<>();
public class LexicalScopeBuilder extends EarlyTerminatingScanner<Object> {
private final List<LexicalScope> allScopes = new ArrayList<>();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think that collecting of all scopes makes sense.
Note that scopes are even changing during scanning. For example:

void draw() {
 //scope1
int a;
//scope2
int b;
//scope3
}

while scanning the statements of method draw, there is only one LexicalScope object whose content is different while scanning each comment.

In all my current use cases (handling of imports #2683) I am scanning AST and I need current lexical scope only.

If you see an use case behind I can live with that. I hope that amount of collected elements is not too big to consume much memory... :-)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation. Fixed.

private final Deque<LexicalScope> scopes = new ArrayDeque<>();
protected void enter(spoon.reflect.declaration.CtElement e) {
NameScope newFinder = onElement(scopes.peek(), e);
LexicalScope newFinder = onElement(scopes.peek(), e);
if (newFinder != null) {
scopes.push(newFinder);
allScopes.add(newFinder);
}
}
protected void exit(spoon.reflect.declaration.CtElement e) {
NameScope topFinder = scopes.peek();
LexicalScope topFinder = scopes.peek();
if (topFinder != null && topFinder.getScopeElement() == e) {
//we are living scope of this ConflictFinder. Pop it
scopes.pop();
Expand All @@ -48,20 +51,29 @@ protected <T> T forEachLocalElementByName(String name, Function<? super CtNamedE
}
};
/**
* @return {@link NameScope} of actually scanned element. The {@link NameScope#forEachElementByName(String, java.util.function.Function)} can be used
* @return {@link LexicalScope} of actually scanned element. The {@link LexicalScope#forEachElementByName(String, java.util.function.Function)} can be used
* to get all {@link CtElement}s which are mapped to that simple name
*/
public NameScope getNameScope() {
NameScope ns = scopes.peek();
public LexicalScope getCurrentNameScope() {
LexicalScope ns = scopes.peek();
return ns == null ? EMPTY : ns;
}

/**
* Returns all the collected name scopes
*
*/
public List<LexicalScope> getNameScopes() {
return Collections.unmodifiableList(allScopes);
}

/**
* Call it for each visited CtElement
* @param parent the parent ConflictFinder
* @param target an element
* @return new {@link AbstractNameScope} if `target` element declares new naming scope or null if there is no new scope
*/
private AbstractNameScope onElement(NameScope parent, CtElement target) {
private AbstractNameScope onElement(LexicalScope parent, CtElement target) {
class Visitor extends CtAbstractVisitor {
AbstractNameScope finder = null;
@Override
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/spoon/reflect/visitor/NameScopeOfType.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@
import spoon.reflect.visitor.filter.AllTypeMembersFunction;

/**
* Represents NameScope of Type. Knows all accessible fields, nested type names and method names
* Represents LexicalScope of Type. Knows all accessible fields, nested type names and method names
*/
class NameScopeOfType extends AbstractNameScope {
private Map<String, CtNamedElement> fieldsByName;
private Map<String, CtNamedElement> typesByName;
private Map<String, CtNamedElement> methodsByName;

NameScopeOfType(NameScope conflictFinder, CtType<?> p_type) {
NameScopeOfType(LexicalScope conflictFinder, CtType<?> p_type) {
super(conflictFinder, p_type);
}

Expand Down
6 changes: 3 additions & 3 deletions src/main/java/spoon/reflect/visitor/SimpleNameScope.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import spoon.reflect.declaration.CtVariable;

/**
* {@link NameScope} for exact set of named elements. There can be:
* {@link LexicalScope} for exact set of named elements. There can be:
* <ul>
* <li>parameter names of constructor, method or lambda
* <li>parameter name of catch variable
Expand All @@ -38,15 +38,15 @@ class SimpleNameScope extends AbstractNameScope {

private final Map<String, CtNamedElement> elementsByName;

SimpleNameScope(NameScope parent, CtElement scopeElement, List<CtParameter<?>> parameters) {
SimpleNameScope(LexicalScope parent, CtElement scopeElement, List<CtParameter<?>> parameters) {
super(parent, scopeElement);
elementsByName = new HashMap<>(parameters.size());
for (CtParameter<?> parameter : parameters) {
elementsByName.put(parameter.getSimpleName(), parameter);
}
}

SimpleNameScope(NameScope parent, CtElement scopeElement) {
SimpleNameScope(LexicalScope parent, CtElement scopeElement) {
super(parent, scopeElement);
elementsByName = new HashMap<>();
}
Expand Down
106 changes: 56 additions & 50 deletions src/test/java/spoon/test/imports/name_scope/NameScopeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,21 @@

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.CtNamedElement;
import spoon.reflect.declaration.CtType;
import spoon.reflect.path.CtRole;
import spoon.reflect.visitor.NameScope;
import spoon.reflect.visitor.NameScopeScanner;
import spoon.reflect.visitor.chain.CtScannerListener;
import spoon.reflect.visitor.chain.ScanningMode;
import spoon.reflect.visitor.LexicalScope;
import spoon.reflect.visitor.LexicalScopeBuilder;
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.
//contract: check that LexicalScope knows expected name.
CtType<?> typeRenata = ModelUtils.buildClass(launcher -> {
//needed to compute imports
launcher.getEnvironment().setAutoImports(true);
Expand All @@ -63,53 +60,62 @@ public void testNameScopeScanner() throws Exception {
CtType<?> typeFiles = typeRenata.getFactory().Type().createReference(Files.class).getTypeDeclaration();
CtMethod<?> methodsNewDirectoryStream = typeFiles.getMethodsByName("newDirectoryStream").get(0);

NameScopeScanner<Void> scanner = new NameScopeScanner<>();
LexicalScopeBuilder scanner = new LexicalScopeBuilder();
scanner.setVisitCompilationUnitContent(true);
scanner.setListener(new CtScannerListener() {
@Override
public ScanningMode enter(CtRole role, CtElement element) {
if (element instanceof CtLiteral) {
CtLiteral<String> literal = (CtLiteral<String>) 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;
}
});
// we collect all scopes
scanner.scan(typeRenata.getPosition().getCompilationUnit());

// we have 8 scopes in Renata
List<LexicalScope> lexicalScopes = scanner.getNameScopes();
assertEquals(8, lexicalScopes.size());

LexicalScope n1 = getNameScope(scanner, "draw");

//contract: the local variables are visible after they are declared
checkThatScopeContains(n1, Arrays.asList(), "count");
checkThatScopeContains(n1, Arrays.asList("String theme"), "theme");
checkThatScopeContains(n1, Arrays.asList(methodDraw), "draw");
checkThatScopeContains(n1, Arrays.asList(typeTereza), "Tereza");
checkThatScopeContains(n1, Arrays.asList(fieldMichal), "michal");
checkThatScopeContains(n1, Arrays.asList(typeRenata), "Renata");
//contract: imported types are visible too
checkThatScopeContains(n1, Arrays.asList(typeFile), "File");
//contract: imported static methods are visible too
checkThatScopeContains(n1, Arrays.asList(methodCurrentTimeMillis), "currentTimeMillis");
//contract: type members imported by wildcard are visible too
checkThatScopeContains(n1, Arrays.asList(methodsNewDirectoryStream), "newDirectoryStream");
//contract: The names are case sensitive
checkThatScopeContains(n1, Arrays.asList(), "Michal");
//the names which are not visible, must not be returned
checkThatScopeContains(n1, Arrays.asList(), "void");
checkThatScopeContains(n1, Arrays.asList(), "String");
//type members of System are not visible
checkThatScopeContains(n1, Arrays.asList(), "setIn");
//type member itself whose field is imported is not visible
checkThatScopeContains(n1, Arrays.asList(), "System");
//type member itself whose type members are imported by wildcard are not visible
checkThatScopeContains(n1, Arrays.asList(), "Fields");

//contract: the local variables is only visible in the block scope (not the method one)
checkThatScopeContains(n1, Arrays.asList(), "count");
checkThatScopeContains(lexicalScopes.get(7), Arrays.asList("int count"), "count");
}


private LexicalScope getNameScope(LexicalScopeBuilder builder, String name) {
for (LexicalScope n: builder.getNameScopes()) {
System.out.println(n.getScopeElement().toString());
if (n.getScopeElement() instanceof CtNamedElement && ((CtNamedElement)n.getScopeElement()).getSimpleName().equals(name)) {
return n;
}
}
throw new IllegalStateException();
}

private void assertNameScope(List<?> expectedElements, NameScope nameScope, String name) {


private void checkThatScopeContains(LexicalScope lexicalScope, List<?> expectedElements, String name) {
List<CtElement> realElements = new ArrayList<>();
nameScope.forEachElementByName(name, e -> realElements.add(e));
lexicalScope.forEachElementByName(name, e -> realElements.add(e));
assertEquals(expectedElements.size(), realElements.size());
for (int i = 0; i < expectedElements.size(); i++) {
Object expected = expectedElements.get(i);
Expand Down