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

Fix NullPointerException resulting from variable accesses in ctors. #1098

Merged
Merged
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
87 changes: 52 additions & 35 deletions src/main/java/spoon/support/compiler/jdt/JDTTreeBuilderHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
package spoon.support.compiler.jdt;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.FieldReference;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.QualifiedNameReference;
import org.eclipse.jdt.internal.compiler.ast.ReferenceExpression;
import org.eclipse.jdt.internal.compiler.ast.SingleNameReference;
Expand All @@ -42,14 +42,16 @@
import spoon.reflect.code.CtTypeAccess;
import spoon.reflect.code.CtVariableAccess;
import spoon.reflect.declaration.CtClass;
import spoon.reflect.declaration.CtConstructor;
import spoon.reflect.declaration.CtExecutable;
import spoon.reflect.declaration.CtField;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.declaration.CtParameter;
import spoon.reflect.declaration.CtType;
import spoon.reflect.declaration.CtVariable;
import spoon.reflect.declaration.ModifierKind;
import spoon.reflect.factory.CoreFactory;
import spoon.reflect.factory.ExecutableFactory;
import spoon.reflect.factory.TypeFactory;
import spoon.reflect.reference.CtArrayTypeReference;
import spoon.reflect.reference.CtExecutableReference;
import spoon.reflect.reference.CtFieldReference;
Expand Down Expand Up @@ -161,6 +163,7 @@ <T> CtVariableAccess<T> createVariableAccess(SingleNameReference singleNameRefer
* visible in current scope, {@code null} otherwise.
*/
<T> CtVariableAccess<T> createVariableAccessNoClasspath(SingleNameReference singleNameReference) {
final TypeFactory typeFactory = jdtTreeBuilder.getFactory().Type();
final CoreFactory coreFactory = jdtTreeBuilder.getFactory().Core();
final ExecutableFactory executableFactory = jdtTreeBuilder.getFactory().Executable();
final ContextBuilder contextBuilder = jdtTreeBuilder.getContextBuilder();
Expand All @@ -186,50 +189,64 @@ <T> CtVariableAccess<T> createVariableAccessNoClasspath(SingleNameReference sing
// references (in terms of Java objects) have not been set up yet. Thus, we need to
// create the required parameter reference by our own.

// since the given parameter has not been declared in a lambda expression it must
// have been declared by a method!
final CtMethod method = (CtMethod) variable.getParent();
// Since the given parameter has not been declared in a lambda expression it must
// have been declared by a method/constructor.
final CtExecutable executable = (CtExecutable) variable.getParent();

// create list of method's parameter types
final List<CtTypeReference<?>> parameterTypesOfMethod = new ArrayList<>();
final List<CtParameter<?>> parametersOfMethod = method.getParameters();
for (CtParameter<?> parameter : parametersOfMethod) {
// create list of executable's parameter types
final List<CtTypeReference<?>> parameterTypesOfExecutable = new ArrayList<>();
@SuppressWarnings("unchecked")
final List<CtParameter<?>> parametersOfExecutable = executable.getParameters();
for (CtParameter<?> parameter : parametersOfExecutable) {
if (parameter.getType() != null) {
parameterTypesOfMethod.add(parameter.getType().clone());
parameterTypesOfExecutable.add(parameter.getType().clone());
} else {
// it's the best match :(
parameterTypesOfExecutable.add(typeFactory.OBJECT);
}
}

// find method's corresponding jdt element
MethodDeclaration methodJDT = null;
// find executable's corresponding jdt element
AbstractMethodDeclaration executableJDT = null;
for (final ASTPair astPair : contextBuilder.stack) {
if (astPair.element == method) {
methodJDT = (MethodDeclaration) astPair.node;
break;
if (astPair.element == executable) {
executableJDT = (AbstractMethodDeclaration) astPair.node;
}
}
assert methodJDT != null;
assert executableJDT != null;

// create a reference to method's declaring class
final CtTypeReference declaringReferenceOfMethod =
// create a reference to executable's declaring class
final CtTypeReference declaringReferenceOfExecutable =
// `binding` may be null for anonymous classes which means we have to
// create an 'empty' type reference since we have no further information
methodJDT.binding == null ? coreFactory.createTypeReference()
: referenceBuilder.getTypeReference(methodJDT.binding.declaringClass);

// create a reference to the method of the currently processed parameter reference
final CtExecutableReference methodReference =
executableFactory.createReference(declaringReferenceOfMethod,
// we need to clone method's return type (rt) before passing to
// `createReference` since this method (indirectly) sets the parent
// of the rt and, therefore, may break the AST
method.getType().clone(),
// no need to clone/copy as Strings are immutable
method.getSimpleName(),
// no need to clone/copy as we just created this object
parameterTypesOfMethod);

// finally, we can set the method reference...
parameterReference.setDeclaringExecutable(methodReference);
// available
executableJDT.binding == null ? coreFactory.createTypeReference()
: referenceBuilder.getTypeReference(
executableJDT.binding.declaringClass);

// If executable is a constructor, `executable.getType()` returns null since the
// parent is not available yet. Fortunately, however, the return type of a
// constructor is its declaring class which, in our case, is already available with
// declaringReferenceOfExecutable.
CtTypeReference executableTypeReference = executable instanceof CtConstructor
// IMPORTANT: Create a clone of the type reference (rt) if retrieved by
// other AST elements as `executableFactory.createReference` (see below)
// indirectly sets the parent of `rt` and, thus, may break the AST!
? declaringReferenceOfExecutable.clone()
: executable.getType().clone();

// create a reference to the executable of the currently processed parameter
// reference
@SuppressWarnings("unchecked")
final CtExecutableReference executableReference =
executableFactory.createReference(
declaringReferenceOfExecutable,
executableTypeReference,
executable.getSimpleName(),
parameterTypesOfExecutable);

// finally, we can set the executable reference...
parameterReference.setDeclaringExecutable(executableReference);
}
variableReference = parameterReference;
variableAccess = isLhsAssignment(contextBuilder, singleNameReference)
Expand Down
9 changes: 9 additions & 0 deletions src/test/java/spoon/test/reference/VariableAccessTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ public void testDeclarationArray() throws Exception {
assertEquals(expected, ((CtVariableAccess) ctArrayWrite.getTarget()).getVariable().getDeclaration());
}

@Test
public void testParameterReferenceInConstructorNoClasspath () {
final Launcher launcher = new Launcher();
// throws `NullPointerException` before PR #1098
launcher.addInputResource("./src/test/resources/noclasspath/org/elasticsearch/indices/analysis/HunspellService.java");
launcher.getEnvironment().setNoClasspath(true);
launcher.buildModel();
}

@Test
public void testDeclarationOfVariableReference() throws Exception {
final Launcher launcher = new Launcher();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch 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.elasticsearch.indices.analysis;

import org.apache.lucene.analysis.hunspell.Dictionary;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.SimpleFSDirectory;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.io.FileSystemUtils;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

/**
* Serves as a node level registry for hunspell dictionaries. This services expects all dictionaries to be located under
* the {@code <path.conf>/hunspell} directory, where each locale has its dedicated sub-directory which holds the dictionary
* files. For example, the dictionary files for {@code en_US} locale must be placed under {@code <path.conf>/hunspell/en_US}
* directory.
* <p>
* The following settings can be set for each dictionary:
* <ul>
* <li>{@code ignore_case} - If true, dictionary matching will be case insensitive (defaults to {@code false})</li>
* <li>{@code strict_affix_parsing} - Determines whether errors while reading a affix rules file will cause exception or simple be ignored
* (defaults to {@code true})</li>
* </ul>
* <p>
* These settings can either be configured as node level configuration, such as:
* <br><br>
* <pre><code>
* indices.analysis.hunspell.dictionary.en_US.ignore_case: true
* indices.analysis.hunspell.dictionary.en_US.strict_affix_parsing: false
* </code></pre>
* <p>
* or, as dedicated configuration per dictionary, placed in a {@code settings.yml} file under the dictionary directory. For
* example, the following can be the content of the {@code <path.config>/hunspell/en_US/settings.yml} file:
* <br><br>
* <pre><code>
* ignore_case: true
* strict_affix_parsing: false
* </code></pre>
*
* @see org.elasticsearch.index.analysis.HunspellTokenFilterFactory
*/
public class HunspellService extends AbstractComponent {

public static final Setting<Boolean> HUNSPELL_LAZY_LOAD =
Setting.boolSetting("indices.analysis.hunspell.dictionary.lazy", Boolean.FALSE, Property.NodeScope);
public static final Setting<Boolean> HUNSPELL_IGNORE_CASE =
Setting.boolSetting("indices.analysis.hunspell.dictionary.ignore_case", Boolean.FALSE, Property.NodeScope);
public static final Setting<Settings> HUNSPELL_DICTIONARY_OPTIONS =
Setting.groupSetting("indices.analysis.hunspell.dictionary.", Property.NodeScope);
private final ConcurrentHashMap<String, Dictionary> dictionaries = new ConcurrentHashMap<>();
private final Map<String, Dictionary> knownDictionaries;
private final boolean defaultIgnoreCase;
private final Path hunspellDir;
private final Function<String, Dictionary> loadingFunction;

public HunspellService(final Settings settings, final Environment env, final Map<String, Dictionary> knownDictionaries)
throws IOException {
super(settings);
this.knownDictionaries = Collections.unmodifiableMap(knownDictionaries);
this.hunspellDir = resolveHunspellDirectory(env);
this.defaultIgnoreCase = HUNSPELL_IGNORE_CASE.get(settings);
this.loadingFunction = (locale) -> {
try {
return loadDictionary(locale, settings, env);
} catch (Exception e) {
throw new IllegalStateException("failed to load hunspell dictionary for locale: " + locale, e);
}
};
if (!HUNSPELL_LAZY_LOAD.get(settings)) {
scanAndLoadDictionaries();
}

}

/**
* Returns the hunspell dictionary for the given locale.
*
* @param locale The name of the locale
*/
public Dictionary getDictionary(String locale) {
Dictionary dictionary = knownDictionaries.get(locale);
if (dictionary == null) {
dictionary = dictionaries.computeIfAbsent(locale, loadingFunction);
}
return dictionary;
}

private Path resolveHunspellDirectory(Environment env) {
return env.configFile().resolve("hunspell");
}

/**
* Scans the hunspell directory and loads all found dictionaries
*/
private void scanAndLoadDictionaries() throws IOException {
if (Files.isDirectory(hunspellDir)) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(hunspellDir)) {
for (Path file : stream) {
if (Files.isDirectory(file)) {
try (DirectoryStream<Path> inner = Files.newDirectoryStream(hunspellDir.resolve(file), "*.dic")) {
if (inner.iterator().hasNext()) { // just making sure it's indeed a dictionary dir
try {
getDictionary(file.getFileName().toString());
} catch (Exception e) {
// The cache loader throws unchecked exception (see #loadDictionary()),
// here we simply report the exception and continue loading the dictionaries
logger.error("exception while loading dictionary {}", e, file.getFileName());
}
}
}
}
}
}
}
}

/**
* Loads the hunspell dictionary for the given local.
*
* @param locale The locale of the hunspell dictionary to be loaded.
* @param nodeSettings The node level settings
* @param env The node environment (from which the conf path will be resolved)
* @return The loaded Hunspell dictionary
* @throws Exception when loading fails (due to IO errors or malformed dictionary files)
*/
private Dictionary loadDictionary(String locale, Settings nodeSettings, Environment env) throws Exception {
if (logger.isDebugEnabled()) {
logger.debug("Loading hunspell dictionary [{}]...", locale);
}
Path dicDir = hunspellDir.resolve(locale);
if (FileSystemUtils.isAccessibleDirectory(dicDir, logger) == false) {
throw new ElasticsearchException(String.format(Locale.ROOT, "Could not find hunspell dictionary [%s]", locale));
}

// merging node settings with hunspell dictionary specific settings
Settings dictSettings = HUNSPELL_DICTIONARY_OPTIONS.get(nodeSettings);
nodeSettings = loadDictionarySettings(dicDir, dictSettings.getByPrefix(locale + "."));

boolean ignoreCase = nodeSettings.getAsBoolean("ignore_case", defaultIgnoreCase);

Path[] affixFiles = FileSystemUtils.files(dicDir, "*.aff");
if (affixFiles.length == 0) {
throw new ElasticsearchException(String.format(Locale.ROOT, "Missing affix file for hunspell dictionary [%s]", locale));
}
if (affixFiles.length != 1) {
throw new ElasticsearchException(String.format(Locale.ROOT, "Too many affix files exist for hunspell dictionary [%s]", locale));
}
InputStream affixStream = null;

Path[] dicFiles = FileSystemUtils.files(dicDir, "*.dic");
List<InputStream> dicStreams = new ArrayList<>(dicFiles.length);
try {

for (int i = 0; i < dicFiles.length; i++) {
dicStreams.add(Files.newInputStream(dicFiles[i]));
}

affixStream = Files.newInputStream(affixFiles[0]);

try (Directory tmp = new SimpleFSDirectory(env.tmpFile())) {
return new Dictionary(tmp, "hunspell", affixStream, dicStreams, ignoreCase);
}

} catch (Exception e) {
logger.error("Could not load hunspell dictionary [{}]", e, locale);
throw e;
} finally {
IOUtils.close(affixStream);
IOUtils.close(dicStreams);
}
}

/**
* Each hunspell dictionary directory may contain a {@code settings.yml} which holds dictionary specific settings. Default
* values for these settings are defined in the given default settings.
*
* @param dir The directory of the dictionary
* @param defaults The default settings for this dictionary
* @return The resolved settings.
*/
private static Settings loadDictionarySettings(Path dir, Settings defaults) throws IOException {
Path file = dir.resolve("settings.yml");
if (Files.exists(file)) {
return Settings.builder().loadFromPath(file).put(defaults).build();
}

file = dir.resolve("settings.json");
if (Files.exists(file)) {
return Settings.builder().loadFromPath(file).put(defaults).build();
}

return defaults;
}
}